Implementing Global Floating Window in Android with AccessibilityService

This article is authorized by Afterschool Kids to be posted on the author’s blog: https://www.jianshu.com/p/b3bd935f8380

Implementing Global Floating Window in Android with AccessibilityService
Effect picture at the top

Introduction

Recently, I am working on a large-screen Android project, a 70-inch full-touch screen, developed based on Android 5.0 AOSP, blocking the three major virtual keys, so I need to fix the sidebar on both sides of the screen to replace the virtual keys and achieve boot auto-start.

I have never developed such a small tool before, so I naturally have no experience. My first reaction was to go to the app market to download similar apps for experience. After using three third-party apps, I found that they all require users to manually open the “Accessibility” feature in the system to simulate the user’s virtual key press events. Through this breakthrough, I googled and found that the service used is AccessibilityService, which is a subclass directly inherited from Service. I went directly to https://developer.android.google.cn/ to find the API and found that applications like WeChat Red Packet Assistant use this service. Amazing!

After the user grants all permissions, can they simulate bank card transfers in minutes? Thinking about it carefully, I started writing bugs, no, I meant writing code.

Project address: https://github.com/afterschoolkido/AndroidSideBar

Start

1. “Boot Start”

This is easy, just listen for boot broadcasts. Define a DeviceBootReceiver that inherits from BroadcastReceiver, and then override the onReceive method to execute your TODO list. My logic here is very simple. At boot, check for authorization. If authorized successfully, directly start the sidebar; if not, go to the authorization settings page:

/**
 * device boot receiver
 *
 * @author majh
 */
public class DeviceBootReceiver extends BroadcastReceiver {

    private static final String ACTION_BOOT = "android.intent.action.BOOT_COMPLETED";

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(ACTION_BOOT)) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if(PermissionUtil.isCanDrawOverlays(context) && PermissionUtil.isAccessibilityServiceEnable(context)) {
                    ServiceGo.launchAccessibility(context);
                }else {
                    MainPageGo(context);
                }
            }else {
                if(PermissionUtil.isAccessibilityServiceEnable(context)) {
                    ServiceGo.launchAccessibility(context);
                }else {
                    MainPageGo(context);
                }
            }
        }
    }

    private void MainPageGo(Context context) {
        Intent launch = new Intent(context,MainActivity.class);
        launch.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(launch);
    }

}

The boot start also needs to request permissions in the manifest file:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

The broadcast receiver also needs to be registered:

<receiver android:name=".DeviceBootReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <category android:name="android.intent.category.HOME"/>
            </intent-filter>
        </receiver>

Note: Domestic mobile phones are equipped with boot auto-start managers, including major manufacturers like Samsung, which are designed to deal with the chaotic auto-start in the domestic market. Here, it is necessary to remind users to manually add the sidebar application to the battery optimization whitelist~

2. “Settings Page”

The settings page is to check the two necessary permissions: “Floating Window” and “Accessibility“. The “Floating Window” permission is directly granted below Android M, so pay attention to this!

/**
 * home page
 *
 * @author majh
 */
public class MainActivity extends AppCompatActivity {

    private AppCompatButton mFlastWindowButton;
    private AppCompatButton mAccessibilityButton;
    private static final int FLAT_REQUEST_CODE = 213;
    private static final int ACCESSIBILITY_REQUEST_CODE = 438;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        setContentView(R.layout.activity_main);
        mFlastWindowButton = findViewById(R.id.btn_flatwindow);
        mAccessibilityButton = findViewById(R.id.btn_accessibility);
        flatWindowVisible();
    }

    /**
     * flut button visible
     */
    private void flatWindowVisible() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            // > M,grant permission
            if (PermissionUtil.isCanDrawOverlays(this)) {
                // permission authorized,service go,button gone
                mFlastWindowButton.setVisibility(View.GONE);
                accessibilityVisible();
            } else {
                // permission unauthorized,button visible
                mFlastWindowButton.setVisibility(View.VISIBLE);
                Toast.makeText(this,getString(R.string.permission_flatwindow_),Toast.LENGTH_SHORT).show();
            }
        } else {
            // < M,service go,gone
            mFlastWindowButton.setVisibility(View.GONE);
            accessibilityVisible();
        }
    }

    /**
     * Accessibility button visible
     */
    private void accessibilityVisible() {
        if(PermissionUtil.isAccessibilityServiceEnable(this)) {
            Toast.makeText(this,getString(R.string.permission_notice),Toast.LENGTH_SHORT).show();
            finish();
        }else {
            mAccessibilityButton.setVisibility(View.VISIBLE);
            Toast.makeText(this,getString(R.string.permission_accessibility_),Toast.LENGTH_SHORT).show();
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.M)
    public void goGetFlatWindow(View view) {
        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
        startActivityForResult(intent,FLAT_REQUEST_CODE);
    }

    public void goGetAccessibility(View view) {
        Intent accessibleIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
        startActivityForResult(accessibleIntent,ACCESSIBILITY_REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(requestCode == FLAT_REQUEST_CODE) {
            flatWindowVisible();
        }else if(requestCode == ACCESSIBILITY_REQUEST_CODE){
            accessibilityVisible();
        }
    }
}

The floating window and accessibility features need to request the following permissions:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
        tools:ignore="ProtectedPermissions" />

Effect picture of settings page:

Implementing Global Floating Window in Android with AccessibilityService

3. “Accessibility Service AccessibilityService”

This is the key point. By using this service and WindowManager, we can directly add views for the sidebars. Using AccessibilityService requires some fixed settings. First, define SideBarService that inherits from AccessibilityService and override several methods. Then, we need to register it in the manifest file as follows:

<service
            android:name=".SideBarService"
            android:enabled="true"
            android:exported="true"
            android:label="@string/app_name"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService"/>
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/auto_reply_service_config"/>
        </service>

It is important to note the @xml/auto_reply_service_config XML file, which is used to set some basic configuration items for AccessibilityService. Of course, it can also be set dynamically in code. I have no special requirements, so I parsed it directly using XML:

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:notificationTimeout="100"
    android:description="@string/accessibility_description"/>

The above @string/accessibility_description (Enable this function to open the sidebar) will be registered in the system, and users can see this description in the phone system’s accessibility settings page:

Implementing Global Floating Window in Android with AccessibilityService

4. “Sidebar”

From the animated image, we can see that the small arrow, sidebar, volume, and brightness control bars are all duplicated. We can’t just copy the same code twice, right?! To simplify, I created three objects for the small arrow, sidebar, and volume brightness, and handled the different performances on both sides within the objects. I’m such a clever little guy ^_~

The code for the small arrow SideBarArrow is as follows:

/**
 * Arrow left & right
 *
 * @author majh
 */
public class SideBarArrow implements View.OnClickListener {

    private WindowManager.LayoutParams mParams;
    private LinearLayout mArrowView;
    private Context mContext;
    private boolean mLeft;
    private WindowManager mWindowManager;
    private SideBarService mSideBarService;
    private SideBarContent mContentBar;
    private LinearLayout mContentBarView;
    private LinearLayout mAnotherArrowView;

    public LinearLayout getView(Context context,boolean left,WindowManager windowManager,SideBarService sideBarService) {
        mContext = context;
        mLeft = left;
        mWindowManager = windowManager;
        mSideBarService = sideBarService;
        mParams = new WindowManager.LayoutParams();
        // compatible
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            mParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        } else {
            mParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
        }
        // set bg transparent
        mParams.format = PixelFormat.RGBA_8888;
        // can not focusable
        mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        mParams.x = 0;
        mParams.y = 0;
        // window size
        mParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
        mParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
        // get layout
        LayoutInflater inflater = LayoutInflater.from(context);
        mArrowView = (LinearLayout) inflater.inflate(R.layout.layout_arrow, null);
        AppCompatImageView arrow = mArrowView.findViewById(R.id.arrow);
        arrow.setOnClickListener(this);
        if(left) {
            arrow.setRotation(180);
            mParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
            mParams.windowAnimations = R.style.LeftSeekBarAnim;
        }else {
            mParams.gravity = Gravity.END | Gravity.CENTER_VERTICAL;
            mParams.windowAnimations = R.style.RightSeekBarAnim;
        }
        mWindowManager.addView(mArrowView,mParams);
        return mArrowView;
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.arrow:
                mArrowView.setVisibility(View.GONE);
                mAnotherArrowView.setVisibility(View.GONE);
                if(null == mContentBar || null == mContentBarView) {
                    mContentBar = new SideBarContent();
                    mContentBarView = mContentBar.getView(mContext,mLeft,mWindowManager,mParams,mArrowView,mSideBarService, mAnotherArrowView);
                }else {
                    mContentBarView.setVisibility(View.VISIBLE);
                }
                mContentBar.removeOrSendMsg(false,true);
                break;
        }
    }

    public void setAnotherArrowBar(LinearLayout anotherArrowBar) {
        mAnotherArrowView = anotherArrowBar;
    }

    public void launcherInvisibleSideBar() {
        mArrowView.setVisibility(View.VISIBLE);
        if(null != mContentBar || null != mContentBarView) {
            mContentBarView.setVisibility(View.GONE);
            mContentBar.removeOrSendMsg(true,false);
            mContentBar.clearSeekBar();
        }
    }

    /**
     * when AccessibilityService is forced closed
     */
    public void clearAll() {
        mWindowManager.removeView(mArrowView);
        if(null != mContentBar || null != mContentBarView) {
            mWindowManager.removeView(mContentBarView);
            mContentBar.clearSeekBar();
            mContentBar.clearCallbacks();
        }
    }
}

As can be seen, when clicking on the Back, Home, and Recent buttons, the corresponding AccessibilityService methods are executed:

mSideBarService.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
mSideBarService.performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME);
mSideBarService.performGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS);

The reason for using AccessibilityService in this project is to call these three methods.

Conclusion

You can also achieve the functionality of the three major virtual keys without using AccessibilityService. The specific code is as follows:

“Back”:

private void back() {
        new Thread() {
            @Override
            public void run() {
                Instrumentation inst = new Instrumentation();
                try {
                    inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.start();
  }

“Home”:

 private void goHome() {
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addCategory(Intent.CATEGORY_HOME);
        getContext().startActivity(intent);
    }

“Recent”:

private void openRecents() {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            try {
                Class serviceManagerClass = Class.forName("android.os.ServiceManager");
                Method getService = serviceManagerClass.getMethod("getService", String.class);
                IBinder retbinder = (IBinder) getService.invoke(serviceManagerClass, "statusbar");
                Class statusBarClass = Class.forName(retbinder.getInterfaceDescriptor());
                Object statusBarObject = statusBarClass.getClasses()[0].getMethod("asInterface", IBinder.class).invoke(null, new Object[]{retbinder});
                Method clearAll = statusBarClass.getMethod("toggleRecentApps");
                clearAll.setAccessible(true);
                clearAll.invoke(statusBarObject);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            Intent intent = new Intent(getContext(), MyAccessibilityService.class);
            intent.putExtra("ACTION", 3);
            getContext().startService(intent);
        }
    }

It can be seen that using the above methods is not only less elegant but also has compatibility issues.

Since this application is placed directly in system/priv-app as a system application, it has root permissions. If your phone has root permissions, granting AccessibilityService can use the following code:

/**
 * device boot receiver
 *
 * @author majh
 */
public class DeviceBootReceiver extends BroadcastReceiver {

    private static final String ACTION_BOOT = "android.intent.action.BOOT_COMPLETED";

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(ACTION_BOOT)) {
            if(!isStartAccessibilityServiceEnable(context)) {
                Settings.Secure.putString(context.getContentResolver(),
                        Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
                        context.getPackageName() + "/" + context.getPackageName() + ".SideBarService");
                Settings.Secure.putInt(context.getContentResolver(),
                        Settings.Secure.ACCESSIBILITY_ENABLED,1);
            }
            ServiceGo.launchAccessibility(context);
        }
    }

    private boolean isStartAccessibilityServiceEnable(Context context){
        AccessibilityManager accessibilityManager =
                (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
        assert accessibilityManager != null;
        List<AccessibilityServiceInfo> accessibilityServices =
                accessibilityManager.getEnabledAccessibilityServiceList(
                        AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
        for (AccessibilityServiceInfo info : accessibilityServices) {
            if (info.getId().contains(context.getPackageName())) {
                return true;
            }
        }
        return false;
    }
}

Thus, a complete Android sidebar floating window is finished. Isn’t it simple?

Project address: https://github.com/afterschoolkido/AndroidSideBar

For the complete source code, please visit the above GitHub address. Welcome to star && fork

Recommended Reading Back in the day, I was young and ignorant, and I cracked a girl game! Using OkHttp3 is not enough; you need to understand these recommendations for a powerful frosted glass effect open-source library.

Programming · Thinking · Career Welcome to scan and follow

Implementing Global Floating Window in Android with AccessibilityService

Leave a Comment

Your email address will not be published. Required fields are marked *