One, the background

This is a sand sculpture operation, because on some Mi phones, playing Toast comes with the name of the app in front of the content of the Toast, like this:

At this point, the product manager spoke up: to unify the style, remove the name of the app before Toast on xiaomi phones.

There are solutions on the Internet. For example, set toast’s message to empty and then set the message to prompt.

MakeText (context, "", totoast.LENGTH_LONG); toast.setText(message); toast.show();Copy the code

But none of these can fundamentally solve the problem, so Hook Toast was born.

Second, analysis

Let’s start by analyzing the process of creating Toast.

A simple use of Toast is as follows:

Toast.makeText(this."abc",Toast.LENGTH_LONG).show();
Copy the code

1. Create toast

MakeText () constructs a Toast as follows:

public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
		@NonNull CharSequence text, @Duration int duration) {
	if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
		Toast result = new Toast(context, looper);
		result.mText = text;
		result.mDuration = duration;
		return result;
	} else {
		Toast result = new Toast(context, looper);
		View v = ToastPresenter.getTextToastView(context, text);
		result.mNextView = v;
		result.mDuration = duration;

		returnresult; }}Copy the code

MakeText (), which sets the length and the text or custom layout to display, is not helpful for hooks.

2. Toast

Let’s move on to Toast’s show():

public void show(a) {... INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView;final int displayId = mContext.getDisplayId();

	try {
		if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
			if(mNextView ! =null) {
				// It's a custom toast
				service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
			} else {
				// It's a text toast
				ITransientNotificationCallback callback =
						newCallbackBinder(mCallbacks, mHandler); service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback); }}else {
            / / show the toastservice.enqueueToast(pkg, mToken, tn, mDuration, displayId); }}catch (RemoteException e) {
		// Empty}}Copy the code

The code is simple and displays the toast mainly through enqueueToast() and enqueueTextToast() of the service.

Service is an object of type INotificationManager, and INotificationManager is an interface, which makes dynamic proxies possible.

GetService () getService() getService() getService() getService()

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
private static INotificationManager sService;

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
static private INotificationManager getService(a) {
	if(sService ! =null) {
		return sService;
	}
	sService = INotificationManager.Stub.asInterface(
			ServiceManager.getService(Context.NOTIFICATION_SERVICE));
	return sService;
}
Copy the code

GetService () ultimately returns sService, which is a lazy singleton, so you can get the actual example by reflection.

3, summary

SService is a singleton that can be retrieved by reflection.

SService implements the INotificationManager interface and can therefore be dynamically proxyed.

Therefore, Hook can be used to interfere with the presentation of Toast.

Three, lu yards

Clear the above process, the implementation is very simple, direct code:

1. Obtain the Field of the sService

Class<Toast> toastClass = Toast.class;

Field sServiceField = toastClass.getDeclaredField("sService");
sServiceField.setAccessible(true);
Copy the code

2. Dynamic proxy replacement

Object proxy = Proxy.newProxyInstance(Thread.class.getClassLoader(), new Class[]{INotificationManager.class}, new InvocationHandler() {
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		
		return null; }});// Assign a value to sService with a proxy object
sServiceField.set(null, proxy);
Copy the code

3. Obtain the original sService object

Since dynamic proxies cannot affect the original process of the object being represented, we need to execute the original logic at the Invoke () of the InvocationHandler() in the second step, which requires obtaining the original object of the sService.

Sservicefield.get (null); sserviceField.get (null); However, it is not available, because the entire Hook operation is in the application initialization, and the entire application has not yet performed toast.show (), so the sService is not initialized (because it is a lazy singleton).

Since you can’t get it directly, let’s call it by reflection:

Method getServiceMethod = toastClass.getDeclaredMethod("getService".null);
getServiceMethod.setAccessible(true);
Object service = getServiceMethod.invoke(null);
Copy the code

Let’s refine the code for step 2:

Object proxy = Proxy.newProxyInstance(Thread.class.getClassLoader(), new Class[]{INotificationManager.class}, new InvocationHandler() {
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		
		returnmethod.invoke(service, args); }});Copy the code

At this point, the proxy for Toast has been implemented, and Toast can perform normally with the original logic, but no additional logic has been added.

4. Add Hook logic

Add additional logic to the InvocationHandler’s invoke() method:

Object proxy = Proxy.newProxyInstance(Thread.class.getClassLoader(), new Class[]{INotificationManager.class}, new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // The action is performed when the enqueueToast() method is determined
        if (method.getName().equals("enqueueToast")) {
            Log.e("hook", method.getName());
            getContent(args[1]);
        }
        returnmethod.invoke(service, args); }});Copy the code

The second part of the args array is an object of type TN, which has an mNextView object of type LinearLayout, and in mNextView there’s a childView of type TextView, which is the TextView that displays the toast text, You can either get its text content directly or assign a value to it, so the code looks like this:

private static void getContent(Object arg) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
	// Get the class of TNClass<? > tnClass = Class.forName(Toast.class.getName() +"$TN");
    // Get the Field of mNextView
    Field mNextViewField = tnClass.getDeclaredField("mNextView");
    mNextViewField.setAccessible(true);
    // Get the mNextView instance
    LinearLayout mNextView = (LinearLayout) mNextViewField.get(arg);
    / / get textview
    TextView childView = (TextView) mNextView.getChildAt(0);
    // Get the text content
    CharSequence text = childView.getText();
    // Replace the text and assign a value
    childView.setText(text.toString().replace("HookToast:".""));
    Log.e("hook"."content: " + childView.getText());
}
Copy the code

A final look at the effect:

Four,

This is a sand carving operation, which is relatively rare in practical application. Hook method can be unified control, and non-invasive. Big guy don’t spray!!