Detailed explanation on how to realize the live broadcast small window of global suspension window such as douyu and station B

When we need to return to or exit from the studio for live broadcast recently, we need to open a small window to continue live video in the whole world, and see the renderings first.

We have investigated the current mainstream live platforms, such as douyu, BiliBili and other apps, which are all made by WindowManger (you can check whether there is suspended window permission in the application permission list, and then disable the douyu permission, and you will be authorized when you return to the douyu live room to exit). That is to say, through WindowManger add, we have a global view, You can apply for permission to hover over all applications to achieve global hover window

ok, after analyzing the implementation principle, we will start to roll up the code

Difficulties in achieving suspension window

1: Permission application: one is that the user needs to authorize manually in 6.0 and later, because the permission of suspension window belongs to high-risk permission, the other is that the permission of MIUI has been modified at the bottom layer, so special processing is needed on Xiaomi mobile phone, and the definition type of permission has changed since 8.0. The following code will explain this in detail

2: For the monitoring of touch events of suspended window, such as click event and touch event, if they are monitored at the same time, setOnclickListener has no effect. You need to distinguish between click and touch, and drag the small window to move. Here is the pointer setting the touch event and click event for the whole window, which will conflict

3: The initialization of the live component, that is, the live window of the global singleton, can encapsulate a user-defined View, This is determined by the respective live sdk. The sdk I use here is in the plug-in, so it's rather cumbersome to implement. But generally, the live sdk (Alibaba cloud or qiniu) can use the same live component object, that is, when the live page is destroyed or returned, the object can be transferred to the small window to realize seamless connection and open the small window live broadcast without reloading, Here, you can use EventBus to send a message or broadcast

1: Authority application

The first step is to declare the hover window permission in the manifest file, the Android manifest file

Then we can trigger the suspension window when the live page returns, that is to say, we can apply for permission when onDestory() or finsh()

Note: after 6.0, it is a high-risk permission, so the code can't get the permission. You need to jump to the permission application list to authorize the user

if (isLiveShow) {
    if (Build.VERSION.SDK_INT >= 23) {
        if (!Settings.canDrawOverlays(getContext())) {
            //No hover window permission, jump application
            Toast.makeText(getApplicationContext(), "Please open the window", Toast.LENGTH_LONG).show();
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
            startActivity(intent);
        } else {
            initLiveWindow();
        }
    } else {
        //Under 6.0, only MUI will modify permissions
        if (MIUI.rom()) {
            if (PermissionUtils.hasPermission(getContext())) {
                initLiveWindow();
            } else {
                MIUI.req(getContext());
            }
        } else {
            initLiveWindow();
        }
    }
}

The lower version generally does not require user authorization except for MIUI, So we need to first determine whether it is a MIUI system, then determine the MIUI version, and then different versions correspond to different permission application postures. If you don't do this, congratulations on not returning the jump permission crash on the low version (lower than 6.0) of Xiaomi mobile phone, because the underlying layer has changed the authorization list class or won't jump the authorization without any response,

//Under 6.0, only MUI will modify permissions
if (MIUI.rom()) {
    if (PermissionUtils.hasPermission(getContext())) {
        initLiveWindow();
    } else {
        MIUI.req(getContext());
    }
} else {
    initLiveWindow();
}

Judge whether it is MIUI system first

public static boolean rom() {
    return Build.MANUFACTURER.equals("Xiaomi");
}

Then according to different versions, different authorization postures

/**
 * Description:
 * Created by PangHaHa on 18-7-25.
 * Copyright (c) 2018 PangHaHa All rights reserved.
 *
 *  /**
 * <p>
 * It should be clear: a MIUI version corresponds to different models of Xiaomi, based on different Android versions, but the permission setting page is related to the MIUI version
 * Type of test:
 * 7.0: 
 * Xiaomi 5 miui8
 * Xiaomi Note2 miui9
 * 6.0.1
 * Xiaomi 5
 * Millet red rice note3
 * 6.0: 
 * Xiaomi 5 success
 * Xiaomi red rice 4A miui8
 * Millet red rice Pro miui7
 * Millet red rice note4 miui8 ------------ failed
 * <p>
 * After a variety of horizontal and vertical test comparison, we come to a conclusion that Xiaomi has no rule to deal with the TYPE_TOAST!
 * It has nothing to do with Android version, nothing to do with MIUI version, and the addView method does not report an error
 * Therefore, the final adaptation method for Xiaomi above 6.0 is: do not use the type ˊ toast type, and apply for permission uniformly
 */

public class MIUI {

    private static final String miui = "ro.miui.ui.version.name";
    private static final String miui5 = "V5";
    private static final String miui6 = "V6";
    private static final String miui7 = "V7";
    private static final String miui8 = "V8";
    private static final String miui9 = "V9";



    public static boolean rom() {
        return Build.MANUFACTURER.equals("Xiaomi");
    }

    private static String getProp() {
        return Rom.getProp(miui);
    }


    public static void req(final Context context) {
        switch (getProp()) {
            case miui5:
                reqForMiui5(context);
                break;
            case miui6:
            case miui7:
                reqForMiui67(context);
                break;
            case miui8:
            case miui9:
                reqForMiui89(context);
                break;
        }

    }


    private static void reqForMiui5(Context context) {
        String packageName = context.getPackageName();
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        Uri uri = Uri.fromParts("package", packageName, null);
        intent.setData(uri);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (isIntentAvailable(intent, context)) {
            context.startActivity(intent);
        }
    }

    private static void reqForMiui67(Context context) {
        Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
        intent.setClassName("com.miui.securitycenter",
                "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
        intent.putExtra("extra_pkgname", context.getPackageName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (isIntentAvailable(intent, context)) {
            context.startActivity(intent);
        }
    }

    private static void reqForMiui89(Context context) {
        Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
        intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
        intent.putExtra("extra_pkgname", context.getPackageName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (isIntentAvailable(intent, context)) {
            context.startActivity(intent);
        } else {
            intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
            intent.setPackage("com.miui.securitycenter");
            intent.putExtra("extra_pkgname", context.getPackageName());
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            if (isIntentAvailable(intent, context)) {
                context.startActivity(intent);
            }
        }
    }


    /**
     * Some models will automatically change to type system alert when TYPE-TOAST type is added. This method can shield modification
     * But... Even if the hover window is displayed successfully, it will collapse if it moves
     */
    private static void addViewToWindow(WindowManager wm, View view, WindowManager.LayoutParams params) {
        setMiUI_International(true);
        wm.addView(view, params);
        setMiUI_International(false);
    }


    private static void setMiUI_International(boolean flag) {
        try {
            Class BuildForMi = Class.forName("miui.os.Build");
            Field isInternational = BuildForMi.getDeclaredField("IS_INTERNATIONAL_BUILD");
            isInternational.setAccessible(true);
            isInternational.setBoolean(null, flag);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

And use the Runtime command getprop to get the version and model of the mobile phone, because different versions of MIUI correspond to different underlying layers and have no rules!

public class Rom {

    static boolean isIntentAvailable(Intent intent, Context context) {
        return intent != null && context.getPackageManager().queryIntentActivities(
                intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
    }


    static String getProp(String name) {
        BufferedReader input = null;
        try {
            Process p = Runtime.getRuntime().exec("getprop " + name);
            input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);
            String line = input.readLine();
            input.close();
            return line;
        } catch (IOException ex) {
            return null;
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Tool class of permission application

public class PermissionUtils {

    public static boolean hasPermission(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            return Settings.canDrawOverlays(context);
        } else {
            return hasPermissionBelowMarshmallow(context);
        }
    }

    public static boolean hasPermissionOnActivityResult(Context context) {
        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) {
            return hasPermissionForO(context);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            return Settings.canDrawOverlays(context);
        } else {
            return hasPermissionBelowMarshmallow(context);
        }
    }

    /**
     * 6.0 Whether the following judgment has authority
     * In theory, only permissions above 6.0 are needed, but some domestic ROMs add permissions below 6.0
     * In fact, this method can also be used to judge the version above 6.0, but it is replaced by a simpler canDrawOverlays
     */
    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    static boolean hasPermissionBelowMarshmallow(Context context) {
        try {
            AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            Method dispatchMethod = AppOpsManager.class.getMethod("checkOp", int.class, int.class, String.class);
            //AppOpsManager.OP_SYSTEM_ALERT_WINDOW = 24
            return AppOpsManager.MODE_ALLOWED == (Integer) dispatchMethod.invoke(
                    manager, 24, Binder.getCallingUid(), context.getApplicationContext().getPackageName());
        } catch (Exception e) {
            return false;
        }
    }


    /**
     * Used to determine whether there is permission at 8.0, only used for OnActivityResult
     * For the 8.0 official bug: after the user grants permission, the Settings.canDrawOverlays or checkOp method still returns false
     */
    @RequiresApi(api = Build.VERSION_CODES.M)
    private static boolean hasPermissionForO(Context context) {
        try {
            WindowManager mgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
            if (mgr == null) return false;
            View viewToAdd = new View(context);
            WindowManager.LayoutParams params = new WindowManager.LayoutParams(0, 0,
                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
                            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
                    WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                    PixelFormat.TRANSPARENT);
            viewToAdd.setLayoutParams(params);
            mgr.addView(viewToAdd, params);
            mgr.removeView(viewToAdd);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

}

2: Initialization of pop-up window and monitoring of touch event

First of all, we need to understand the source code of windows manger. There are only three methods

package android.view;

/** Interface to let you add and remove child views to an Activity. To get an instance
  * of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
  */
public interface ViewManager
{
    /**
     * Assign the passed LayoutParams to the passed View and add the view to the window.
     * <p>Throws {@link android.view.WindowManager.BadTokenException} for certain programming
     * errors, such as adding a second view to a window without removing the first view.
     * <p>Throws {@link android.view.WindowManager.InvalidDisplayException} if the window is on a
     * secondary {@link Display} and the specified display can't be found
     * (see {@link android.app.Presentation}).
     * @param view The view to be added to this window.
     * @param params The LayoutParams to assign to view.
     */
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

Just look at the name, add, update, delete

Then we need to customize a View and add it to windows manager through addView. First, we need to add the key code Two points need to be noted

A. After 8.0, the permission definition has changed and the type needs to be modified

//Set type. The system prompt window is generally above the application window
if (Build.VERSION.SDK_INT >= 26) { //8 new features
    params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
    params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}

B. The concept of reference system and initial coordinate. The reference system Gravity refers to the point of origin rather than the position of the initial pop-up window relative to the screen! The Gravity attribute should be noted: Note: Gravity does not mean that the views you add to WindowManager are placed relative to the screens, But you can set up your reference system! For example: mWinParams.gravity= Gravity.LEFT | Gravity.TOP; It means that if the upper left corner of the screen is taken as the reference system, then the coordinate of the upper left corner of the screen is (0,0), This is the only basis for placing the View position behind you. When you set mWinParams.gravity = Gravity.CENTER; So your screen center is the reference system, coordinate (0,0). Generally we use the upper left corner of the screen as the reference system

C. For the handling of touch events, we View the corresponding touch events before passing them to the onClick click event. If the touch is blocked, it will not be passed to the next level

1. We add offset through the position after finger movement, and then windows manger calls update view layout to update the interface to achieve real-time dragging to change the position

2. By calculating the position of the last touch screen and the offset of this touch screen, the offset of x-axis and y-axis is less than 2 pixels, which is considered as a click event, and the click event of the whole window is executed, otherwise the touch event of the whole window is executed

//Actively calculate the width and height information of the current View
toucherLayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);

//Processing touch
toucherLayout.setOnTouchListener(new View.OnTouchListener() {@Override public boolean onTouch(View view, MotionEvent event) {

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isMoved = false;
            // Record the pressed position
            lastX = event.getRawX();
            lastY = event.getRawY();

            start_X = event.getRawX();
            start_Y = event.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            isMoved = true;
            // Record the position after moving
            float moveX = event.getRawX();
            float moveY = event.getRawY();
            // Get the layout attribute of the current window, add the offset, update the interface, and move
            params.x += (int)(moveX - lastX);
            params.y += (int)(moveY - lastY);
            windowManager.updateViewLayout(toucherLayout, params);

            lastX = moveX;
            lastY = moveY;
            break;
        case MotionEvent.ACTION_UP:

            float fmoveX = event.getRawX();
            float fmoveY = event.getRawY();

            if (Math.abs(fmoveX - start_X) < offset && Math.abs(fmoveY - start_Y) < offset) {
                isMoved = false;
                remove(context);
                leaveCast(context);
                String PARAM_CIRCLE_ID = "param_circle_id";
                Intent intent = new Intent();
                intent.putExtra(PARAM_CIRCLE_ID, circle_id);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.setComponent(new ComponentName(RePlugin.getHostContext().getPackageName(), "com.sina.licaishicircle.sections.circledetail.CircleActivity"));
                context.startActivity(intent);
            } else {
                isMoved = true;
            }
            break;
        }
        // If it is a mobile event, it will be consumed; if it is not, it will be handled by others, such as clicking
        return isMoved;
    }

3: Global single live broadcast and reuse of live window construction

Because 360 Replugin plug-in management mode is used in the project, and the live components are all in plug-ins, it is necessary to obtain the live pop-up tool class by reflection

public class LiveWindowUtil {

    private static class Hold {
        public static LiveWindowUtil instance = new LiveWindowUtil();
    }

    public static LiveWindowUtil getInstance() {
        return Hold.instance;
    }

    public LiveWindowUtil() {
        //Code using plug-in Fragment
        RePlugin.fetchContext("sina.com.cn.courseplugin");
    }

    private Object o;
    private Class clazz;
    public void init(Context context, Map map) {
        try {
            ClassLoader classLoader = RePlugin.fetchClassLoader("sina.com.cn.courseplugin");//Get ClassLoader of plug-in
            clazz = classLoader.loadClass("sina.com.cn.courseplugin.tools.LiveUtils");
            o = clazz.newInstance();
            Method method = clazz.getMethod("initLive", Context.class, Map.class);
            method.invoke(o, context, map);

        }catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }catch (NullPointerException e){
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

    public void remove(Context context) {
        Method method = null;
        try {
            if(clazz != null && o != null) {
                method = clazz.getMethod("remove", Context.class);
                method.invoke(o,context);
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }
}

To sum up, it is mainly necessary to get the permission, and then transfer the live component to the small window to monitor the touch event of the suspended window. The pit of permission is relatively large. Except for MIUI, other brands of mobile phones may also have less than 6.0, which is inexplicable.

Original author: pangha 12138, original link: https://www.jianshu.com/p/e953f5b924e1

Welcome to my WeChat official account, "code breakout", sharing technology like Python, Java, big data, machine learning, AI, etc., focusing on code farming technology upgrading, workplace breakout, thinking leap, 200 thousand + code farm growth charging station, and growing up with you.

Tags: MIUI Android Mobile SDK

Posted on Fri, 20 Mar 2020 09:18:14 -0700 by stry_cat