IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> tinode客户端安卓版编译手账 -> 正文阅读

[移动开发]tinode客户端安卓版编译手账

前一阵子我自己架设了一个tinode的IM服务器,

web直接可以运行

但是安卓版本的一直报错,

具体信息为:

No subjectAltNames on the certificate match

问了作者,作者竟然把我的问题直接删除了,还是自己调试代码吧。毕竟源码面前,了无秘密;

一、代码地址

GitHub - tinode/tindroid: Tinode chat client application for Android

我从release部分下载了0.20.9版本源码

二、更改源码配置

1)根目录下的build.gradle有2处需要更改,主要是版本信息,非git版本无从提取,随便设置一下

static def gitVersionCode() {
    // If you are not compiling in a git directory and getting an error like
    // [A problem occurred evaluating root project 'master'. For input string: ""]
    // then just return your manually assigned error code like this:
    //  return 12345
    def process = "git rev-list --count HEAD".execute()
    return 12345
}

// Use current git tag as a version name.
// For example, if the git tag is 'v0.20.0-rc1' then the version name will be '0.20.0-rc1'.
static def gitVersionName() {
    // If you are not compiling in a git directory, you should manually assign version name:
    //  return "MyVersionName"
    def process = "git describe --tags".execute()
    // Remove trailing CR and remove leading 'v' as in 'v1.2.3'
    return "1.2.3"
}

2)app下面的build.gradle有3处需要修改

2.1)程序使用googleService,需要去官网注册一下相关的资料,自己注册一个新的应用,下载得到google-services.json,这个文件放置于app目录;

2.2)google-services.json中我们注册了一个应用的名字,这文件中有个package_name替换原来的应用ID,否则编译不过

applicationId "com.birdschat.cn"

2.3)创建证书,文件放置于源码同级目录,比如我的:

../robinkeys/key.keystore

在根目录下添加一个配置文件,叫keystore.properties,内容大概如下:

keystoreFile=../robin_keys/key.keystore
keystoreAlias=key.keystore
keystorePassword=123456
keyPassword=123456

并根据自己配置文件中的参数名,设置一下build.gradle:

signingConfigs {
        release {
            storeFile file(keystoreProperties['keystoreFile'])
            storePassword keystoreProperties['keystorePassword']
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
        }
    }

这样应该就可以编译了!!

3)取消客户端WebSocket 的SSL双向认证

但是运行后,设置了自己的服务器,以及使用加密模式,无法注册或者登录,

主要是我们的证书需要有域名,并且是申请来的,也就是有CA认证的,而不是自己生成的,不然无法实现双向验证,这主要是为了防止中间人攻击;

但是我们往往就是自己内部试用,不需要这么麻烦,

需要对SDK部分代码进行更该,参考:java websocket及忽略证书_nell_lee的博客-CSDN博客_websocket 忽略证书

更改后的代码如下:Connection.java 全文

package co.tinode.tinodesdk;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CancellationException;

import co.tinode.tinodesdk.model.MsgServerCtrl;
import co.tinode.tinodesdk.model.ServerMessage;
//
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.*;
import java.net.Socket;
import java.net.URI;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

public class LargeFileHelper {
    private static final int BUFFER_SIZE = 65536;
    private static final String TWO_HYPHENS = "--";
    private static final String BOUNDARY = "*****" + System.currentTimeMillis() + "*****";
    private static final String LINE_END = "\r\n";

    private final URL mUrlUpload;
    private final String mHost;
    private final String mApiKey;
    private final String mAuthToken;
    private final String mUserAgent;

    private boolean mCanceled = false;

    private int mReqId = 1;

    public LargeFileHelper(URL urlUpload, String apikey, String authToken, String userAgent) {
        mUrlUpload = urlUpload;
        mHost = mUrlUpload.getHost();
        mApiKey = apikey;
        mAuthToken = authToken;
        mUserAgent = userAgent;
        handleSSLHandshake();
    }

    // robin add here
    public static void handleSSLHandshake() {
        try {
            TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }

                @Override
                public void checkClientTrusted(X509Certificate[] certs, String authType) {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) {
                }
            }};

            SSLContext sc = SSLContext.getInstance("TLS");
            // trustAllCerts信任所有的证书
            sc.init(null, trustAllCerts, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
            HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            });
        } catch (Exception ignored) {
        }
    }



    // Upload file out of band. This should not be called on the UI thread.
    public ServerMessage upload(@NotNull InputStream in, @NotNull String filename, @NotNull String mimetype, long size,
                                @Nullable String topic, @Nullable FileHelperProgress progress) throws IOException, CancellationException {
        mCanceled = false;
        HttpURLConnection conn = null;
        ServerMessage msg;
        try {
            conn = (HttpURLConnection) mUrlUpload.openConnection();

            conn.setDoOutput(true);
            conn.setUseCaches(false);
            conn.setRequestProperty("Connection", "Keep-Alive");
            conn.setRequestProperty("User-Agent", mUserAgent);
            conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
            conn.setRequestProperty("X-Tinode-APIKey", mApiKey);
            if (mAuthToken != null) {
                // mAuthToken could be null when uploading avatar on sign up.
                conn.setRequestProperty("X-Tinode-Auth", "Token " + mAuthToken);
            }
            conn.setChunkedStreamingMode(0);

            DataOutputStream out = new DataOutputStream(new BufferedOutputStream(conn.getOutputStream()));
            // Write req ID.
            out.writeBytes(TWO_HYPHENS + BOUNDARY + LINE_END);
            out.writeBytes("Content-Disposition: form-data; name=\"id\"" + LINE_END);
            out.writeBytes(LINE_END);
            out.writeBytes(++mReqId + LINE_END);

            // Write topic.
            if (topic != null) {
                out.writeBytes(TWO_HYPHENS + BOUNDARY + LINE_END);
                out.writeBytes("Content-Disposition: form-data; name=\"topic\"" + LINE_END);
                out.writeBytes(LINE_END);
                out.writeBytes(topic + LINE_END);
            }

            // File section.
            out.writeBytes(TWO_HYPHENS + BOUNDARY + LINE_END);
            // Content-Disposition: form-data; name="file"; filename="1519014549699.pdf"
            out.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"" + LINE_END);

            // Content-Type: application/pdf
            out.writeBytes("Content-Type: " + mimetype + LINE_END);
            out.writeBytes("Content-Transfer-Encoding: binary" + LINE_END);
            out.writeBytes(LINE_END);

            // File bytes.
            copyStream(in, out, size, progress);
            out.writeBytes(LINE_END);

            // End of form boundary.
            out.writeBytes(TWO_HYPHENS + BOUNDARY + TWO_HYPHENS + LINE_END);
            out.flush();
            out.close();

            if (conn.getResponseCode() != 200) {
                throw new IOException("Failed to upload: " + conn.getResponseMessage() +
                        " (" + conn.getResponseCode() + ")");
            }

            InputStream resp = new BufferedInputStream(conn.getInputStream());
            msg = readServerResponse(resp);
            resp.close();
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
        return msg;
    }

    // Uploads the file using Runnable, returns PromisedReply. Safe to call on UI thread.
    public PromisedReply<ServerMessage> uploadFuture(final InputStream in,
                                                     final String filename,
                                                     final String mimetype,
                                                     final long size,
                                                     final String topic,
                                                     final FileHelperProgress progress) {
        final PromisedReply<ServerMessage> result = new PromisedReply<>();
        new Thread(() -> {
            try {
                ServerMessage msg = upload(in, filename, mimetype, size, topic, progress);
                if (mCanceled) {
                    throw new CancellationException("Cancelled");
                }
                result.resolve(msg);
            } catch (Exception ex) {
                try {
                    result.reject(ex);
                } catch (Exception ignored) {
                }
            }
        }).start();
        return result;
    }

    // Download file from the given URL if the URL's host is the default host. Should not be called on the UI thread.
    public long download(String downloadFrom, OutputStream out, FileHelperProgress progress)
            throws IOException, CancellationException {
        URL url = new URL(downloadFrom);
        long size = 0;
        String scheme = url.getProtocol();
        if (!scheme.equals("http") && !scheme.equals("https")) {
            // As a security measure refuse to download using non-http(s) protocols.
            return size;
        }
        HttpURLConnection urlConnection = null;
        try {
            urlConnection = (HttpURLConnection) url.openConnection();
            if (url.getHost().equals(mHost)) {
                // Send authentication only if the host is known.
                urlConnection.setRequestProperty("X-Tinode-APIKey", mApiKey);
                urlConnection.setRequestProperty("X-Tinode-Auth", "Token " + mAuthToken);
            }
            InputStream in = new BufferedInputStream(urlConnection.getInputStream());
            return copyStream(in, out, urlConnection.getContentLength(), progress);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
    }

    // Downloads the file using Runnable, returns PromisedReply. Safe to call on UI thread.
    public PromisedReply<Long> downloadFuture(final String downloadFrom,
                                                 final OutputStream out,
                                                 final FileHelperProgress progress) {
        final PromisedReply<Long> result = new PromisedReply<>();
        new Thread(() -> {
            try {
                Long size = download(downloadFrom, out, progress);
                if (mCanceled) {
                    throw new CancellationException("Cancelled");
                }
                result.resolve(size);
            } catch (Exception ex) {
                try {
                    result.reject(ex);
                } catch (Exception ignored) {
                }
            }
        }).start();
        return result;
    }

    // Try to cancel an ongoing upload or download.
    public void cancel() {
        mCanceled = true;
    }

    public boolean isCanceled() {
        return mCanceled;
    }

    private int copyStream(@NotNull InputStream in, @NotNull OutputStream out, long size, @Nullable FileHelperProgress p)
            throws IOException, CancellationException {
        byte[] buffer = new byte[BUFFER_SIZE];
        int len, sent = 0;
        while ((len = in.read(buffer)) != -1) {
            if (mCanceled) {
                throw new CancellationException("Cancelled");
            }

            sent += len;
            out.write(buffer, 0, len);

            if (mCanceled) {
                throw new CancellationException("Cancelled");
            }

            if (p != null) {
                p.onProgress(sent, size);
            }
        }
        return sent;
    }

    private ServerMessage readServerResponse(InputStream in) throws IOException {
        MsgServerCtrl ctrl = null;
        ObjectMapper mapper = Tinode.getJsonMapper();
        JsonParser parser = mapper.getFactory().createParser(in);
        if (parser.nextToken() != JsonToken.START_OBJECT) {
            throw new JsonParseException(parser, "Packet must start with an object",
                    parser.getCurrentLocation());
        }
        if (parser.nextToken() != JsonToken.END_OBJECT) {
            String name = parser.getCurrentName();
            parser.nextToken();
            JsonNode node = mapper.readTree(parser);
            if (name.equals("ctrl")) {
                ctrl = mapper.readValue(node.traverse(), MsgServerCtrl.class);
            } else {
                throw new JsonParseException(parser, "Unexpected message '" + name + "'",
                        parser.getCurrentLocation());
            }
        }
        return new ServerMessage(ctrl);
    }

    public interface FileHelperProgress {
        void onProgress(long sent, long size);
    }

    public Map<String,String> headers() {
        Map<String,String> headers = new HashMap<>();
        headers.put("X-Tinode-APIKey", mApiKey);
        headers.put("X-Tinode-Auth", "Token " + mAuthToken);
        return headers;
    }
}

这样,登录就OK了;

4)设置服务器默认参数

将服务器的链接参数预先设置好为我们需要的:

4.1) 地址与端口:全文搜索“:6060”字样,在资源文件res/strings.xml中更改:

<string name="emulator_host_name" translatable="false">119.0.0.1:6060</string>

同时,将build.gradle的相关位置做更改,自动生成相关的资源文件

buildTypes {
        debug {
            resValue "string", "default_host_name", '"119.0.0.0:6060"'
        }
        release {
            resValue "string", "default_host_name", '"api.tinode.co"'
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }

在同样在资源中更改为自己的地址和端口;

4.2)默认使用https,更改TindroidApp.java

将返回的默认的参数设置为true

 public static boolean getDefaultTLS() {
        //return !isEmulator();
        return true;
    }

编译好了就可以用了!

5) 还需要更改LargeFileHelper,

在完成第4步骤后,发送小文件正常,大文件比如5兆,就报错了,?

 java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

这说明大文件使用其他的方法发送时验证证书失败了,果然,SDK中使用了单独的一个辅助类单独开了一个链接POST发送大文件,参考如下链接:Trust anchor for certification path not found异常解决方法_HZYXN的博客-CSDN博客

更改后的代码如下:

package co.tinode.tinodesdk;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CancellationException;

import co.tinode.tinodesdk.model.MsgServerCtrl;
import co.tinode.tinodesdk.model.ServerMessage;
//
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.*;
import java.net.Socket;
import java.net.URI;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

public class LargeFileHelper {
    private static final int BUFFER_SIZE = 65536;
    private static final String TWO_HYPHENS = "--";
    private static final String BOUNDARY = "*****" + System.currentTimeMillis() + "*****";
    private static final String LINE_END = "\r\n";

    private final URL mUrlUpload;
    private final String mHost;
    private final String mApiKey;
    private final String mAuthToken;
    private final String mUserAgent;

    private boolean mCanceled = false;

    private int mReqId = 1;

    public LargeFileHelper(URL urlUpload, String apikey, String authToken, String userAgent) {
        mUrlUpload = urlUpload;
        mHost = mUrlUpload.getHost();
        mApiKey = apikey;
        mAuthToken = authToken;
        mUserAgent = userAgent;
        handleSSLHandshake();
    }

    // robin add here
    public static void handleSSLHandshake() {
        try {
            TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }

                @Override
                public void checkClientTrusted(X509Certificate[] certs, String authType) {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) {
                }
            }};

            SSLContext sc = SSLContext.getInstance("TLS");
            // trustAllCerts信任所有的证书
            sc.init(null, trustAllCerts, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
            HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            });
        } catch (Exception ignored) {
        }
    }



    // Upload file out of band. This should not be called on the UI thread.
    public ServerMessage upload(@NotNull InputStream in, @NotNull String filename, @NotNull String mimetype, long size,
                                @Nullable String topic, @Nullable FileHelperProgress progress) throws IOException, CancellationException {
        mCanceled = false;
        HttpURLConnection conn = null;
        ServerMessage msg;
        try {
            conn = (HttpURLConnection) mUrlUpload.openConnection();

            conn.setDoOutput(true);
            conn.setUseCaches(false);
            conn.setRequestProperty("Connection", "Keep-Alive");
            conn.setRequestProperty("User-Agent", mUserAgent);
            conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
            conn.setRequestProperty("X-Tinode-APIKey", mApiKey);
            if (mAuthToken != null) {
                // mAuthToken could be null when uploading avatar on sign up.
                conn.setRequestProperty("X-Tinode-Auth", "Token " + mAuthToken);
            }
            conn.setChunkedStreamingMode(0);

            DataOutputStream out = new DataOutputStream(new BufferedOutputStream(conn.getOutputStream()));
            // Write req ID.
            out.writeBytes(TWO_HYPHENS + BOUNDARY + LINE_END);
            out.writeBytes("Content-Disposition: form-data; name=\"id\"" + LINE_END);
            out.writeBytes(LINE_END);
            out.writeBytes(++mReqId + LINE_END);

            // Write topic.
            if (topic != null) {
                out.writeBytes(TWO_HYPHENS + BOUNDARY + LINE_END);
                out.writeBytes("Content-Disposition: form-data; name=\"topic\"" + LINE_END);
                out.writeBytes(LINE_END);
                out.writeBytes(topic + LINE_END);
            }

            // File section.
            out.writeBytes(TWO_HYPHENS + BOUNDARY + LINE_END);
            // Content-Disposition: form-data; name="file"; filename="1519014549699.pdf"
            out.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"" + LINE_END);

            // Content-Type: application/pdf
            out.writeBytes("Content-Type: " + mimetype + LINE_END);
            out.writeBytes("Content-Transfer-Encoding: binary" + LINE_END);
            out.writeBytes(LINE_END);

            // File bytes.
            copyStream(in, out, size, progress);
            out.writeBytes(LINE_END);

            // End of form boundary.
            out.writeBytes(TWO_HYPHENS + BOUNDARY + TWO_HYPHENS + LINE_END);
            out.flush();
            out.close();

            if (conn.getResponseCode() != 200) {
                throw new IOException("Failed to upload: " + conn.getResponseMessage() +
                        " (" + conn.getResponseCode() + ")");
            }

            InputStream resp = new BufferedInputStream(conn.getInputStream());
            msg = readServerResponse(resp);
            resp.close();
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
        return msg;
    }

    // Uploads the file using Runnable, returns PromisedReply. Safe to call on UI thread.
    public PromisedReply<ServerMessage> uploadFuture(final InputStream in,
                                                     final String filename,
                                                     final String mimetype,
                                                     final long size,
                                                     final String topic,
                                                     final FileHelperProgress progress) {
        final PromisedReply<ServerMessage> result = new PromisedReply<>();
        new Thread(() -> {
            try {
                ServerMessage msg = upload(in, filename, mimetype, size, topic, progress);
                if (mCanceled) {
                    throw new CancellationException("Cancelled");
                }
                result.resolve(msg);
            } catch (Exception ex) {
                try {
                    result.reject(ex);
                } catch (Exception ignored) {
                }
            }
        }).start();
        return result;
    }

    // Download file from the given URL if the URL's host is the default host. Should not be called on the UI thread.
    public long download(String downloadFrom, OutputStream out, FileHelperProgress progress)
            throws IOException, CancellationException {
        URL url = new URL(downloadFrom);
        long size = 0;
        String scheme = url.getProtocol();
        if (!scheme.equals("http") && !scheme.equals("https")) {
            // As a security measure refuse to download using non-http(s) protocols.
            return size;
        }
        HttpURLConnection urlConnection = null;
        try {
            urlConnection = (HttpURLConnection) url.openConnection();
            if (url.getHost().equals(mHost)) {
                // Send authentication only if the host is known.
                urlConnection.setRequestProperty("X-Tinode-APIKey", mApiKey);
                urlConnection.setRequestProperty("X-Tinode-Auth", "Token " + mAuthToken);
            }
            InputStream in = new BufferedInputStream(urlConnection.getInputStream());
            return copyStream(in, out, urlConnection.getContentLength(), progress);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
    }

    // Downloads the file using Runnable, returns PromisedReply. Safe to call on UI thread.
    public PromisedReply<Long> downloadFuture(final String downloadFrom,
                                                 final OutputStream out,
                                                 final FileHelperProgress progress) {
        final PromisedReply<Long> result = new PromisedReply<>();
        new Thread(() -> {
            try {
                Long size = download(downloadFrom, out, progress);
                if (mCanceled) {
                    throw new CancellationException("Cancelled");
                }
                result.resolve(size);
            } catch (Exception ex) {
                try {
                    result.reject(ex);
                } catch (Exception ignored) {
                }
            }
        }).start();
        return result;
    }

    // Try to cancel an ongoing upload or download.
    public void cancel() {
        mCanceled = true;
    }

    public boolean isCanceled() {
        return mCanceled;
    }

    private int copyStream(@NotNull InputStream in, @NotNull OutputStream out, long size, @Nullable FileHelperProgress p)
            throws IOException, CancellationException {
        byte[] buffer = new byte[BUFFER_SIZE];
        int len, sent = 0;
        while ((len = in.read(buffer)) != -1) {
            if (mCanceled) {
                throw new CancellationException("Cancelled");
            }

            sent += len;
            out.write(buffer, 0, len);

            if (mCanceled) {
                throw new CancellationException("Cancelled");
            }

            if (p != null) {
                p.onProgress(sent, size);
            }
        }
        return sent;
    }

    private ServerMessage readServerResponse(InputStream in) throws IOException {
        MsgServerCtrl ctrl = null;
        ObjectMapper mapper = Tinode.getJsonMapper();
        JsonParser parser = mapper.getFactory().createParser(in);
        if (parser.nextToken() != JsonToken.START_OBJECT) {
            throw new JsonParseException(parser, "Packet must start with an object",
                    parser.getCurrentLocation());
        }
        if (parser.nextToken() != JsonToken.END_OBJECT) {
            String name = parser.getCurrentName();
            parser.nextToken();
            JsonNode node = mapper.readTree(parser);
            if (name.equals("ctrl")) {
                ctrl = mapper.readValue(node.traverse(), MsgServerCtrl.class);
            } else {
                throw new JsonParseException(parser, "Unexpected message '" + name + "'",
                        parser.getCurrentLocation());
            }
        }
        return new ServerMessage(ctrl);
    }

    public interface FileHelperProgress {
        void onProgress(long sent, long size);
    }

    public Map<String,String> headers() {
        Map<String,String> headers = new HashMap<>();
        headers.put("X-Tinode-APIKey", mApiKey);
        headers.put("X-Tinode-Auth", "Token " + mAuthToken);
        return headers;
    }
}

编译后,发送大文件不再报错了。

6)还需要发送图片时,无法正常弹出图片浏览器框

调试过程中发现,是由于我们更改了应用程序的ID,造成默认的路径发生了变化,在读取临时文件时候发生了错误,所以需要更改一下相关的路径:provider_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path name="tindroid_downloads" path="./Download" />
    <external-path name="tindroid_images" path="Android/data/com.birdschat.cn/files/Pictures" />
</paths>

这里应该与applicationId中设置的一样才行。

其实这里的错误,经过分析代码,是因为MessagesFragment.java 在试图发送图片时候,程序会尝试新建一个文件用于保存照片,所以才会使用了临时文件,而这里由于逻辑问题,在真机上每次即使不拍照也会产生垃圾数据,所以决定禁用了拍照,选择时候反而更加流程;

7)聊天头像丢失的问题

这个其实也是认证的问题,主要是在ChatsAdapter.java中bing函数设置avatar时候,使用了piccaso来下载对应的图片,但是不能直接改;因为在TindroidApp.java中初始化时候设置了Okhttp3下载工具,并且设置了相对链接的转换方式,

所以应该改TindroidApp.java

package co.tinode.tindroid;

import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.util.Log;

import com.google.firebase.crashlytics.FirebaseCrashlytics;
import com.squareup.picasso.OkHttp3Downloader;
import com.squareup.picasso.Picasso;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Map;

import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.preference.PreferenceManager;
import androidx.work.WorkManager;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import co.tinode.tindroid.account.ContactsObserver;
import co.tinode.tindroid.account.Utils;
import co.tinode.tindroid.db.BaseDb;
import co.tinode.tinodesdk.ComTopic;
import co.tinode.tinodesdk.ServerResponseException;
import co.tinode.tinodesdk.Storage;
import co.tinode.tinodesdk.Tinode;
import co.tinode.tinodesdk.model.MsgServerData;
import co.tinode.tinodesdk.model.MsgServerInfo;

import okhttp3.OkHttpClient;
import okhttp3.Request;

/**
 * A class for providing global context for database access
 */
public class TindroidApp extends Application implements DefaultLifecycleObserver {
    private static final String TAG = "TindroidApp";

    // 256 MB.
    private static final int PICASSO_CACHE_SIZE = 1024 * 1024 * 256;

    private static TindroidApp sContext;

    private static ContentObserver sContactsObserver = null;

    // The Tinode cache is linked from here so it's never garbage collected.
    @SuppressWarnings({"FieldCanBeLocal", "unused"})
    private static Cache sCache;

    private static String sAppVersion = null;
    private static int sAppBuild = 0;

    //private static String sServerHost = null;
    //private static boolean sUseTLS = false;

    public TindroidApp() {
        sContext = this;
    }

    public static Context getAppContext() {
        return sContext;
    }

    public static String getAppVersion() {
        return sAppVersion;
    }

    public static int getAppBuild() {
        return sAppBuild;
    }

    public static String getDefaultHostName(Context context) {
        return context.getResources().getString(isEmulator() ?
                R.string.emulator_host_name :
                R.string.default_host_name);
    }

    public static boolean getDefaultTLS() {
        //return !isEmulator();
        return true;
    }

    public static void retainCache(Cache cache) {
        sCache = cache;
    }

    // Detect if the code is running in an emulator.
    // Used mostly for convenience to use correct server address i.e. 10.0.2.2:6060 vs sandbox.tinode.co and
    // to enable/disable Crashlytics. It's OK if it's imprecise.
    public static boolean isEmulator() {
        return Build.FINGERPRINT.startsWith("sdk_gphone_x86")
                || Build.FINGERPRINT.startsWith("unknown")
                || Build.MODEL.contains("google_sdk")
                || Build.MODEL.contains("Emulator")
                || Build.MODEL.contains("Android SDK built for x86")
                || Build.MANUFACTURER.contains("Genymotion")
                || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
                || "google_sdk".equals(Build.PRODUCT)
                || Build.PRODUCT.startsWith("sdk")
                || Build.PRODUCT.startsWith("vbox");
    }

    static synchronized void startWatchingContacts(Context context, Account acc) {
        if (sContactsObserver == null) {
            // Check if we have already obtained contacts permissions.
            if (!UiUtils.isPermissionGranted(context, Manifest.permission.READ_CONTACTS)) {
                // No permissions, can't set up contacts sync.
                return;
            }

            // Create and start a new thread set up as a looper.
            HandlerThread thread = new HandlerThread("ContactsObserverHandlerThread");
            thread.start();

            sContactsObserver = new ContactsObserver(acc, new Handler(thread.getLooper()));
            // Observer which triggers sync when contacts change.
            sContext.getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI,
                    true, sContactsObserver);
        }
    }

    static synchronized void stopWatchingContacts() {
        if (sContactsObserver != null) {
            sContext.getContentResolver().unregisterContentObserver(sContactsObserver);
        }
    }

    // robin add
    public static OkHttpClient getUnsafeOkHttpClient(Context context){
        try {
            final TrustManager[] trustAllCerts = new TrustManager[]{
                    new X509TrustManager() {
                        @Override
                        public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

                        }

                        @Override
                        public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

                        }

                        @Override
                        public X509Certificate[] getAcceptedIssuers() {
                            return new X509Certificate[]{};
                        }
                    }
            };

            X509TrustManager x509TrustManager = (X509TrustManager) trustAllCerts[0];
            final SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, trustAllCerts, new SecureRandom());
            final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
            OkHttpClient.Builder builder = new OkHttpClient.Builder();
            builder.sslSocketFactory(sslSocketFactory, x509TrustManager);


            builder.hostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String s, SSLSession sslSession) {
                    return true;
                }
            });

            builder.cache(new okhttp3.Cache(createDefaultCacheDir(context), PICASSO_CACHE_SIZE))
                    .addInterceptor(chain -> {
                        Tinode tinode = Cache.getTinode();
                        Request picassoReq = chain.request();
                        Map<String, String> headers;
                        if (tinode.isTrustedURL(picassoReq.url().url())) {
                            headers = tinode.getRequestHeaders();
                            Request.Builder builder1 = picassoReq.newBuilder();
                            for (Map.Entry<String, String> el : headers.entrySet()) {
                                builder1 = builder1.addHeader(el.getKey(), el.getValue());
                            }
                            return chain.proceed(builder1.build());
                        } else {
                            return chain.proceed(picassoReq);
                        }
                    });
            return builder.build();

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        catch (KeyManagementException e){
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        try {
            PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0);
            sAppVersion = pi.versionName;
            if (TextUtils.isEmpty(sAppVersion)) {
                sAppVersion = BuildConfig.VERSION_NAME;
            }
            sAppBuild = pi.versionCode;
            if (sAppBuild <= 0) {
                sAppBuild = BuildConfig.VERSION_CODE;
            }
        } catch (PackageManager.NameNotFoundException e) {
            Log.w(TAG, "Failed to retrieve app version", e);
        }

        // Disable Crashlytics for debug builds.
        FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG);

        BroadcastReceiver br = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String token = intent.getStringExtra("token");
                if (token != null && !token.equals("")) {
                    Cache.getTinode().setDeviceToken(token);
                }
            }
        };
        LocalBroadcastManager.getInstance(this).registerReceiver(br, new IntentFilter("FCM_REFRESH_TOKEN"));

        createNotificationChannel();

        ProcessLifecycleOwner.get().getLifecycle().addObserver(this);

        // Check if preferences already exist. If not, create them.
        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
        if (TextUtils.isEmpty(pref.getString(Utils.PREFS_HOST_NAME, null))) {
            // No preferences found. Save default values.
            SharedPreferences.Editor editor = pref.edit();
            editor.putString(Utils.PREFS_HOST_NAME, getDefaultHostName(this));
            editor.putBoolean(Utils.PREFS_USE_TLS, getDefaultTLS());
            editor.apply();
        }
        // Event handlers for video calls.
        Cache.getTinode().addListener(new Tinode.EventListener() {
            @Override
            public void onDataMessage(MsgServerData data) {
                if (Cache.getTinode().isMe(data.from)) {
                    return;
                }
                String webrtc = data.getStringHeader("webrtc");
                if (MsgServerData.parseWebRTC(webrtc) != MsgServerData.WebRTC.STARTED) {
                    return;
                }
                ComTopic topic = (ComTopic) Cache.getTinode().getTopic(data.topic);
                if (topic == null) {
                    return;
                }

                // Check if we have a later version of the message (which means the call
                // has been not yet been either accepted or finished).
                Storage.Message msg = topic.getMessage(data.seq);
                if (msg != null) {
                    webrtc = msg.getStringHeader("webrtc");
                    if (webrtc != null && MsgServerData.parseWebRTC(webrtc) != MsgServerData.WebRTC.STARTED) {
                        return;
                    }
                }

                CallInProgress call = Cache.getCallInProgress();
                if (call == null) {
                    // Call invite from the peer.
                    Intent intent = new Intent();
                    intent.setAction(CallActivity.INTENT_ACTION_CALL_INCOMING);
                    intent.putExtra("topic", data.topic);
                    intent.putExtra("seq", data.seq);
                    intent.putExtra("from", data.from);
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    TindroidApp.this.startActivity(intent);
                } else if (!call.equals(data.topic, data.seq)) {
                    // Another incoming call. Decline.
                    topic.videoCallHangUp(data.seq);
                }
            }

            @Override
            public void onInfoMessage(MsgServerInfo info) {
                if (MsgServerInfo.parseWhat(info.what) != MsgServerInfo.What.CALL) {
                    return;
                }
                if (MsgServerInfo.parseEvent(info.event) != MsgServerInfo.Event.ACCEPT) {
                    return;
                }

                CallInProgress call = Cache.getCallInProgress();
                if (Tinode.TOPIC_ME.equals(info.topic) && Cache.getTinode().isMe(info.from) &&
                    call != null && call.equals(info.src, info.seq)) {
                    // Another client has accepted the call. Dismiss call notification.
                    LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(TindroidApp.this);
                    Intent intent = new Intent(CallActivity.INTENT_ACTION_CALL_CLOSE);
                    intent.putExtra("topic", info.src);
                    intent.putExtra("seq", info.seq);
                    lbm.sendBroadcast(intent);
                }
            }
        });

        // Clear completed/failed upload tasks.
        WorkManager.getInstance(this).pruneWork();

        // Setting up Picasso with auth headers.
//        OkHttpClient client = new OkHttpClient.Builder()
//                .cache(new okhttp3.Cache(createDefaultCacheDir(this), PICASSO_CACHE_SIZE))
//                .addInterceptor(chain -> {
//                    Tinode tinode = Cache.getTinode();
//                    Request picassoReq = chain.request();
//                    Map<String, String> headers;
//                    if (tinode.isTrustedURL(picassoReq.url().url())) {
//                        headers = tinode.getRequestHeaders();
//                        Request.Builder builder = picassoReq.newBuilder();
//                        for (Map.Entry<String, String> el : headers.entrySet()) {
//                            builder = builder.addHeader(el.getKey(), el.getValue());
//                        }
//                        return chain.proceed(builder.build());
//                    } else {
//                        return chain.proceed(picassoReq);
//                    }
//                })
//                .build();
        // note here
        Picasso.setSingletonInstance(new Picasso.Builder(this)
                .requestTransformer(request -> {
                    // Rewrite relative URIs to absolute.
                    if (request.uri != null && Tinode.isUrlRelative(request.uri.toString())) {
                        URL url = Cache.getTinode().toAbsoluteURL(request.uri.toString());
                        if (url != null) {
                            return request.buildUpon().setUri(Uri.parse(url.toString())).build();
                        }
                    }
                    return request;
                })
                .downloader(new OkHttp3Downloader(getUnsafeOkHttpClient(this)))
                .build());

        // Listen to connectivity changes.
        ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
        if (cm == null) {
            return;
        }
        NetworkRequest req = new NetworkRequest.
                Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build();
        cm.registerNetworkCallback(req, new ConnectivityManager.NetworkCallback() {
            @Override
            public void onAvailable(@NonNull Network network) {
                super.onAvailable(network);
                if (!TextUtils.isEmpty(BaseDb.getInstance().getUid())) {
                    // Connect right away if UID is available.
                    Cache.getTinode().reconnectNow(true, false, false);
                }
            }
        });
    }

    static File createDefaultCacheDir(Context context) {
        File cache = new File(context.getApplicationContext().getCacheDir(), "picasso-cache");
        if (!cache.exists()) {
            // noinspection ResultOfMethodCallIgnored
            cache.mkdirs();
        }
        return cache;
    }

    @Override
    public void onStart(@NonNull LifecycleOwner owner) {
        // Check if the app has an account already. If so, initialize the shared connection with the server.
        // Initialization may fail if device is not connected to the network.
        String uid = BaseDb.getInstance().getUid();
        if (!TextUtils.isEmpty(uid)) {
            new LoginWithSavedAccount().execute(uid);
        }
    }

    @Override
    public void onStop(@NonNull LifecycleOwner owner) {
        // Disconnect now, so the connection does not wait for the timeout.
        if (Cache.getTinode() != null) {
            Cache.getTinode().maybeDisconnect(false);
        }
    }

    private void createNotificationChannel() {
        // Create the NotificationChannel on API 26+
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel("new_message",
                    getString(R.string.notification_channel_name), NotificationManager.IMPORTANCE_DEFAULT);
            channel.setDescription(getString(R.string.notification_channel_description));
            NotificationManager nm = getSystemService(NotificationManager.class);
            if (nm != null) {
                nm.createNotificationChannel(channel);
            }
        }
    }

    // Read saved account credentials and try to connect to server using them.
    // Suppressed lint warning because TindroidApp won't leak: it must exist for the entire lifetime of the app.
    @SuppressLint("StaticFieldLeak")
    private class LoginWithSavedAccount extends AsyncTask<String, Void, Void> {
        @Override
        protected Void doInBackground(String... uidWrapper) {
            final AccountManager accountManager = AccountManager.get(TindroidApp.this);
            final Account account = Utils.getSavedAccount(accountManager, uidWrapper[0]);
            if (account != null) {
                // Check if sync is enabled.
                if (ContentResolver.getMasterSyncAutomatically()) {
                    if (!ContentResolver.getSyncAutomatically(account, Utils.SYNC_AUTHORITY)) {
                        ContentResolver.setSyncAutomatically(account, Utils.SYNC_AUTHORITY, true);
                    }
                }

                // Account found, establish connection to the server and use save account credentials for login.
                String token = null;
                Date expires = null;
                try {
                    token = accountManager.blockingGetAuthToken(account, Utils.TOKEN_TYPE, false);
                    String strExp = accountManager.getUserData(account, Utils.TOKEN_EXPIRATION_TIME);
                    // FIXME: remove this check when all clients are updated; Apr 8, 2020.
                    if (!TextUtils.isEmpty(strExp)) {
                        expires = new Date(Long.parseLong(strExp));
                    }
                } catch (OperationCanceledException e) {
                    Log.i(TAG, "Request to get an existing account was canceled.", e);
                } catch (AuthenticatorException e) {
                    Log.e(TAG, "No access to saved account", e);
                } catch (Exception e) {
                    Log.e(TAG, "Failure to login with saved account", e);
                }

                // Must instantiate tinode cache even if token == null. Otherwise logout won't work.
                final Tinode tinode = Cache.getTinode();
                if (!TextUtils.isEmpty(token) && expires != null && expires.after(new Date())) {
                    // Connecting with synchronous calls because this is not the UI thread.
                    tinode.setAutoLoginToken(token);
                    // Connect and login.
                    try {
                        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(TindroidApp.this);
                        // Sync call throws on error.
                        tinode.connect(pref.getString(Utils.PREFS_HOST_NAME, getDefaultHostName(TindroidApp.this)),
                                pref.getBoolean(Utils.PREFS_USE_TLS, getDefaultTLS()),
                                false).getResult();
                        if (!tinode.isAuthenticated()) {
                            // The connection may already exist but not yet authenticated.
                            tinode.loginToken(token).getResult();
                        }
                        Cache.attachMeTopic(null);
                        // Logged in successfully. Save refreshed token for future use.
                        accountManager.setAuthToken(account, Utils.TOKEN_TYPE, tinode.getAuthToken());
                        accountManager.setUserData(account, Utils.TOKEN_EXPIRATION_TIME,
                                String.valueOf(tinode.getAuthTokenExpiration().getTime()));
                        startWatchingContacts(TindroidApp.this, account);
                        // Trigger sync to be sure contacts are up to date.
                        UiUtils.requestImmediateContactsSync(account);
                    } catch (IOException ex) {
                        Log.d(TAG, "Network failure during login", ex);
                        // Do not invalidate token on network failure.
                    } catch (ServerResponseException ex) {
                        Log.w(TAG, "Server rejected login sequence", ex);
                        int code = ex.getCode();
                        // 401: Token expired or invalid login.
                        // 404: 'me' topic is not found (user deleted, but token is still valid).
                        if (code == 401 || code == 404) {
                            // Another try-catch because some users revoke needed permission after granting it.
                            try {
                                // Login failed due to invalid (expired) token or missing/disabled account.
                                accountManager.invalidateAuthToken(Utils.ACCOUNT_TYPE, null);
                                accountManager.setUserData(account, Utils.TOKEN_EXPIRATION_TIME, null);
                            } catch (SecurityException ex2) {
                                Log.e(TAG, "Unable to access android account", ex2);
                            }
                            // Force new login.
                            UiUtils.doLogout(TindroidApp.this);
                        }
                        // 409 Already authenticated should not be possible here.
                    } catch (Exception ex) {
                        Log.e(TAG, "Other failure during login", ex);
                    }
                } else {
                    Log.i(TAG, "No token or expired token. Forcing re-login");
                    try {
                        if (!TextUtils.isEmpty(token)) {
                            accountManager.invalidateAuthToken(Utils.ACCOUNT_TYPE, null);
                        }
                        accountManager.setUserData(account, Utils.TOKEN_EXPIRATION_TIME, null);
                    } catch (SecurityException ex) {
                        Log.e(TAG, "Unable to access android account", ex);
                    }
                    // Force new login.
                    UiUtils.doLogout(TindroidApp.this);
                }
            } else {
                Log.i(TAG, "Account not found or no permission to access accounts");
                // Force new login in case account existed before but was deleted.
                UiUtils.doLogout(TindroidApp.this);
            }
            return null;
        }
    }
}

头像已经可以出现了,

?8) 还剩下一个问题,就是有时候某些图片发送好像失败了,

待查

结束。

备注:编译好的apk?https://download.csdn.net/download/robinfoxnan/87300700

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-12-25 11:21:31  更:2022-12-25 11:23:59 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/20 11:27:21-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码