Creating a system overlay window (always on top) – Dev

The best answers to the question “Creating a system overlay window (always on top)” in the category Dev.

QUESTION:

I’m trying to create an always-op-top button/clickable-image
which stays on top of all the windows all the time.

The proof of
concept is

  • here – Smart Taskbar (on AppBrain)
  • and here V1.4.0 Sidebar style SWKey – Button savior (on xda-developers)

I have been successful and have a running service now. The service
displays some text on top left corner of screen all the time while
user can freely interact with rest of apps in normal manner.

What I’m
doing is subclass ViewGroup and add it to root window manager with
flag TYPE_SYSTEM_OVERLAY. Now I want to add a button/clickable-image
in place of this text which can receive touch events on itself. I
tried overriding “onTouchEvent” for the whole ViewGroup but it does
not receive any event.

How can I receive events only on certain parts
of my always-on-top view group? Kindly suggest.

public class HUD extends Service {
    HUDView mView;

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Toast.makeText(getBaseContext(),"onCreate", Toast.LENGTH_LONG).show();
        mView = new HUDView(this);
        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
                0,
//              WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
//                      | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                PixelFormat.TRANSLUCENT);
        params.gravity = Gravity.RIGHT | Gravity.TOP;
        params.setTitle("Load Average");
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        wm.addView(mView, params);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Toast.makeText(getBaseContext(),"onDestroy", Toast.LENGTH_LONG).show();
        if(mView != null)
        {
            ((WindowManager) getSystemService(WINDOW_SERVICE)).removeView(mView);
            mView = null;
        }
    }
}

class HUDView extends ViewGroup {
    private Paint mLoadPaint;

    public HUDView(Context context) {
        super(context);
        Toast.makeText(getContext(),"HUDView", Toast.LENGTH_LONG).show();

        mLoadPaint = new Paint();
        mLoadPaint.setAntiAlias(true);
        mLoadPaint.setTextSize(10);
        mLoadPaint.setARGB(255, 255, 0, 0);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText("Hello World", 5, 15, mLoadPaint);
    }

    @Override
    protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //return super.onTouchEvent(event);
        Toast.makeText(getContext(),"onTouchEvent", Toast.LENGTH_LONG).show();
        return true;
    }
}

ANSWER:

Following @Sam Lu’s answer,
Indeed Android 4.x blocks certain types from listening to outside touch events, but some types, such as TYPE_SYSTEM_ALERT, still do the job.

Example

    WindowManager.LayoutParams params = new WindowManager.LayoutParams(
            WindowManager.LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
            PixelFormat.TRANSLUCENT);

    WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
    LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
    View myView = inflater.inflate(R.layout.my_view, null);
    myView.setOnTouchListener(new OnTouchListener() {
       @Override
       public boolean onTouch(View v, MotionEvent event) {
           Log.d(TAG, "touch me");
           return true;
       }
     });

    // Add layout to window manager
    wm.addView(myView, params);

Permissions

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

ANSWER:

This might be a stupid solution. But it works. If you can improve it, please let me know.

OnCreate of your Service: I have used WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH flag. This is the only change in service.

@Override
    public void onCreate() {
        super.onCreate();
        Toast.makeText(getBaseContext(),"onCreate", Toast.LENGTH_LONG).show();
        mView = new HUDView(this);
        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
                WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
                PixelFormat.TRANSLUCENT);
        params.gravity = Gravity.RIGHT | Gravity.TOP;
        params.setTitle("Load Average");
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        wm.addView(mView, params);
    }

Now, you will start getting each and every click event. So, you need to rectify in your event handler.

In your ViewGroup touch event

@Override
public boolean onTouchEvent(MotionEvent event) {

    // ATTENTION: GET THE X,Y OF EVENT FROM THE PARAMETER
    // THEN CHECK IF THAT IS INSIDE YOUR DESIRED AREA


    Toast.makeText(getContext(),"onTouchEvent", Toast.LENGTH_LONG).show();
    return true;
}

Also you may need to add this permission to your manifest:

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

ANSWER:

I’m one of the developers of the Tooleap SDK. We also provide a way for developers to display always on top windows and buttons, and and we have dealt with a similar situation.

One problem the answers here haven’t addressed is that of the Android “Secured Buttons”.

Secured buttons have the filterTouchesWhenObscured property which means they can’t be interacted with, if placed under a window, even if that window does not receive any touches. Quoting the Android documentation:

Specifies whether to filter touches when the view’s window is obscured
by another visible window. When set to true, the view will not receive
touches whenever a toast, dialog or other window appears above the
view’s window. Refer to the {@link android.view.View} security
documentation for more details.

An example of such a button is the install button when you try to install third party apks. Any app can display such a button if adding to the view layout the following line:

android:filterTouchesWhenObscured="true"

If you display an always-on-top window over a “Secured Button”, so all the secured button parts that are covered by an overlay will not handle any touches, even if that overlay is not clickable. So if you are planing to display such a window, you should provide a way for the user to move it or dismiss it.
And if a part of your overlay is transparent, take into account that your user might be confused why is a certain button in the underlying app is not working for him suddenly.

ANSWER:

Starting with Android 4.x, Android team fixed a potential
security problem by adding a new function adjustWindowParamsLw() in which it
will add FLAG_NOT_FOCUSABLE, FLAG_NOT_TOUCHABLE and remove FLAG_WATCH_OUTSIDE_TOUCH flags for TYPE_SYSTEM_OVERLAY windows.

That is, a TYPE_SYSTEM_OVERLAY window won’t receive any touch event on ICS platform and, of course, to use TYPE_SYSTEM_OVERLAY is not a workable solution for ICS and future devices.