Sunday, November 18, 2012

Toggle Android Toast Message Manually

Android Toast shows a popup notification message for a short time then automatically fades out. Following code example will show a Toast when you touch the phone scree:

public class MainActivity extends Activity implements OnClickListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.mainLayout).setOnClickListener(this);
    }
    
    @Override
    public void onClick(View v) {
        Toast.makeText(this, "Toast Message", Toast.LENGTH_LONG).show();
    }
}

From the latest Toast implementation source code I noticed that the Toast message is controlled by INotificationManager:

    /**
     * Show the view for the specified duration.
     */
    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }
We can see the message text is set to a TN object. TN is an inner class that actually shows or hides the Toast message:
     private static class TN extends ITransientNotification.Stub {
          TN() {
            // XXX This should be changed to use a Dialog, with a Theme.Toast
            // defined that sets up the layout params appropriately.
            final WindowManager.LayoutParams params = mParams;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                    | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
            params.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
        }

        /**
         * schedule handleShow into the right thread
         */
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }

        /**
         * schedule handleHide into the right thread
         */
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }
Use reflection we can access the internal TN object, bypass the INotificationManager and show/hide the Toast message manually. Instead of fading out automatically, the result is a permanent Toast message on the screen.

Following code illustrates how to toggle Toast message manually where the Toast Message shows up when you touch the screen, and the message will stay on the screen unless you touch the screen again:

public class MainActivity extends Activity implements OnClickListener {
    private boolean isToastShowing = false;
    private Toast toast;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.mainLayout).setOnClickListener(this);
    }
    
    @Override
    public void onClick(View v) {
        //Toast.makeText(this, "Toast Message", Toast.LENGTH_LONG).show();
        if (toast == null) {
            toast = Toast.makeText(this, "Toast Message", Toast.LENGTH_LONG);
        }
        Field mTNField;
        try {
            mTNField = toast.getClass().getDeclaredField("mTN");
            mTNField.setAccessible(true);
            Object obj = mTNField.get(toast);
            Method method;
            if (isToastShowing) {
                method = obj.getClass().getDeclaredMethod("hide", null);
                isToastShowing = false;
            } else {
                method = obj.getClass().getDeclaredMethod("show", null);
                isToastShowing = true;
            }
            method.invoke(obj, null);
        } catch (Exception e) {
            e.printStackTrace();
        }        
    }
}