Android WeChat H5 download file processing: Let WeChat automatically pop up and jump to the external browser window

Supporting video: https://www.bilibili.com/video/BV1oA411B7gv/


background

Today, I fiddled with projecting the screen from my mobile phone to my laptop, and I wanted to record a video to show my learning results. I just remembered this function that I realized a long time ago.
H5 file download is a very simple function, but if this H5 is opened on the Android version of WeChat, the function cannot be used, because the built-in browser of WeChat on the Android side intercepts all requests for downloading files.
Even WeChat's sdk does not provide an interface for directly saving files, so there is only one way out, which is to jump to a third-party application for download, such as jumping to a mobile browser or a WeChat applet. If it is an app that has been put on the App Store, you can jump to the App Store to download it.
The reason for the blocking should be the reason why H5 cannot be supervised, but what is incomprehensible is that WeChat on the ios side can be downloaded. Is the Apple mobile phone superior to others?

solution collection

The final solution chosen

  • It was an accident to think of this plan.

  • At the beginning, I only tested the download of zip, and it really couldn't be downloaded, so that I thought that all formats couldn't be downloaded, so I turned to Baidu to find the answer.

    image-20230211165914298
  • Then the test told me that some ios files cannot be previewed or downloaded.

  • So I left this hole and went to solve the ios problem first.

  • Baidu found that ios is also a fake download. It first opens the file in a preview mode, and the user needs to click the upper right corner to save it manually.

  • Moreover, the file suffix and the response header content-typemust correspond strictly, otherwise an error will be reported, and the preview cannot be previewed.

  • Reference: Solve the problem that the H5 download file on the mobile terminal prompts that the file type is unrecognizable or illegal

  • After fixing the ios problem, I uploaded files in various formats and tested it. After confirming the fix, I switched back to the Android side.

  • I clicked a few times casually, and it was just a few clicks that gave me hope.

    image-20230211170009537
  • Not all types of files cannot be downloaded. For docx, pdf, xlsx, txt and other formats, WeChat will actively evoke the selection pop-up window to jump to other browsers.

  • This is undoubtedly much friendlier than the front-end writing prompt window.

  • So as long as we use the excellent quality of stealth and abduction, let WeChat treat all files equally, and all the jump windows will be aroused.

  • So far, the problem of downloading files with H5 on the Android side has been perfectly solved.

  • The method of deception is also very simple. Anyway, WeChat cannot be downloaded, and all download requests are given to it with a fake file, such as 123456.xlsx.

Java implementation

  • Note that if the interface uses cookie authentication and jumps to an external browser, the cookie cannot be carried over.
  • It is necessary to provide an interface that does not require authentication, and use another method of authentication, such as time sharing code or directly carrying sessionId.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
public class ApiController {
    
    

    // 获取日志对象 Spring Boot 中内置了日志框架 Slf4j
    private static Logger log = LoggerFactory.getLogger(ApiController.class);

    /**
     * 处理微信文件下载
     * 欺骗安卓微信唤起打开外部浏览器的选择框
     * ios微信可以预览每种格式的文件,但是不支持直接下载,需要用户在预览页点右上角手动保存
     * 另外,ios对content_type要求严格,如果文件后缀和content_type对不上,连预览页都进不了
     * 企业微信不用做任何处理
     */
    @GetMapping("downloadFileWx")
    public void downloadFileWx(@RequestParam String path, HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    
        responseOutputFileWx(path, null, request, response);
    }

    /**
     * 响应文件流
     * @param path 文件路径
     * @param outputFileName 文件名称,赋值给响应头Content-Disposition
     * @param request
     * @param response
     */
    public void responseOutputFileWx(String path, String outputFileName,
                                   HttpServletRequest request, HttpServletResponse response)
            throws Exception {
    
    
        File file = new File(path);
        if (file == null || !file.exists() || !file.isFile()) {
    
    
            log.error("文件不存在");
            // 重定向到当前页面,相当于刷新页面
            String contextPath = request.getContextPath();
            response.sendRedirect(contextPath + "/downFile");
            return;
        }

        if (outputFileName == null || outputFileName.trim().length() == 0) {
    
    
            // 假如下载文件名参数为空,则设置为原始文件名
            outputFileName = file.getName();
        }
        ServletContext context = request.getServletContext();
        // 文件绝对路径
        String absolutePath = file.getAbsolutePath();
        // 获取文件的MIME type
        String mimeType = context.getMimeType(absolutePath);
        if (mimeType == null) {
    
    
            // 没有发现则设为二进制流
            mimeType = "application/octet-stream";
        }

        response.setContentType(mimeType);
        // 设置文件下载响应头
        String headerKey = "Content-Disposition";
        String headerValue = null;

        if (isWx(request)) {
    
    
            // 微信浏览器,打开手机默认浏览器下载文件
            // 注意排除企业微信
            try {
    
    
                if (isAndroidWx(request)) {
    
    
                    // 安卓端,xlsx文件类型会触发微信弹出跳转外部浏览器窗口,欺骗一下
                    response.setContentType("application/octet-stream");
                    outputFileName = "123456.xlsx";
                } else {
    
    
                    // ios 微信对contentType要求比较严格
                    // https://juejin.cn/post/6844904086463053837
                    if (absolutePath.endsWith("xlsx")) {
    
    
                        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
                    } else if (absolutePath.endsWith("xls")) {
    
    
                        response.setContentType("application/vnd.ms-excel");
                    } else if (absolutePath.endsWith("doc")) {
    
    
                        response.setContentType("application/msword");
                    } else if (absolutePath.endsWith("docx")) {
    
    
                        response.setContentType("application/application/vnd.openxmlformats-officedocument.wordprocessingml.document");
                    }
                }
                headerValue = String.format("attachment; filename=\"%s\"", URLEncoder.encode(outputFileName, "UTF-8"));
            } catch (Exception e) {
    
    
                headerValue = String.format("attachment; filename=\"%s\"", outputFileName);
                log.error(e.getMessage(), e);
            }
        } else {
    
    
            try {
    
    
                // 解决Firefox浏览器中文件名中文乱码
                // https://blog.csdn.net/Jon_Smoke/article/details/53699400
                headerValue = String.format("attachment; filename* = UTF-8''%s",
                        URLEncoder.encode(outputFileName, "UTF-8")
                );
            } catch (Exception e) {
    
    
                headerValue = String.format("attachment; filename=\"%s\"", outputFileName);
                log.error(e.getMessage(), e);
            }
        }
        response.setHeader(headerKey, headerValue);

        String fileName = file.getName();
        try (OutputStream outputStream = response.getOutputStream()) {
    
    
            response.setCharacterEncoding("utf-8");

            // 将下面2行放开,可以测试微信最原始反应
            // 设置返回类型
            // response.setContentType("multipart/form-data");
            // // 文件名转码一下,不然会出现中文乱码
            // response.setHeader("Content-Disposition", "attachment;fileName=" + encodeStr(fileName));

            byte[] bytes = readBytes(file);
            if (bytes == null) {
    
    
                log.error("文件不存在");
            }
            outputStream.write(bytes);
            log.info("文件下载成功!" + fileName);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    /**
     * 对字符串(文件名或路径)进行url编码
     */
    private String encodeStr(String str) throws Exception {
    
    
        return URLEncoder.encode(str, "UTF-8");
    }

    /**
     * 将文件转为byte数组
     */
    public byte[] readBytes(File file) throws Exception {
    
    
        long len = file.length();
        // 无论数组的类型如何,数组中的最大元素数为Integer.MAX_VALUE,大约20亿
        if (len >= 2147483647L) {
    
    
            return null;
        } else {
    
    
            byte[] bytes = new byte[(int) len];

            try (FileInputStream in = new FileInputStream(file)) {
    
    
                int readLength = in.read(bytes);
                if ((long) readLength < len) {
    
    
                    log.error("文件未读取完全");
                    return null;
                }
            } catch (Exception var10) {
    
    
                return null;
            }

            return bytes;
        }
    }

    /**
     * 是否从安卓端微信请求,需要排除企业微信
     */
    private static boolean isAndroidWx(HttpServletRequest request) {
    
    
        String userAgent = request.getHeader("user-agent");
        return userAgent != null && userAgent.toLowerCase().indexOf("micromessenger") > -1
                && userAgent.toLowerCase().indexOf("wxwork") < 0
                && userAgent.toLowerCase().indexOf("android") > -1;
    }

    /**
     * 是否从微信请求,需要排除企业微信
     * 安卓或ios
     */
    private static boolean isWx(HttpServletRequest request) {
    
    
        String userAgent = request.getHeader("user-agent");
        return userAgent != null && userAgent.toLowerCase().indexOf("micromessenger") > -1
                && userAgent.toLowerCase().indexOf("wxwork") < 0;
    }

}

Digression: How to mirror a laptop from a mobile phone

Method 1: win10 comes with screen projection

  • Press "Windows logo key + I" to open Settings, Settings –> System –> Project to this PC

  • Projecting to this computer is either grayed out or not available, or "We are confirming this feature"

  • The first time you need to install a wireless display

  • Use the built-in function of the computer to cast the screen on the mobile phone

  • After the mobile phone is mirrored to the notebook, the notebook will be hijacked, that is, only the screen of the mobile phone can be operated, and the mouse cannot be moved out. You can use the mouse on the computer to directly operate the mobile phone.

  • This is a bit inconvenient. For example, if you want to preview the effect of the mobile phone while writing code, it cannot be realized.

  • In addition, it is recommended to select only the first time verification is required. I voted successfully for the first time, but after I closed it, I couldn’t vote for it. The main reason is that the notebook cannot pop up a confirmation dialog box.

  • After that, restart the computer to successfully cast the screen for the second time.

Method 2: ScreenShare software

Guess you like

Origin blog.csdn.net/weixin_44174211/article/details/128985936