前一阵子我自己架设了一个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 12345def 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 herepublic static void handleSSLHandshake() {try {TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {public X509Certificate[] getAcceptedIssuers() {return new X509Certificate[0];}@Overridepublic void checkClientTrusted(X509Certificate[] certs, String authType) {}@Overridepublic 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() {@Overridepublic 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/pdfout.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 trueshrinkResources trueproguardFiles 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 herepublic static void handleSSLHandshake() {try {TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {public X509Certificate[] getAcceptedIssuers() {return new X509Certificate[0];}@Overridepublic void checkClientTrusted(X509Certificate[] certs, String authType) {}@Overridepublic 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() {@Overridepublic 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/pdfout.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 addpublic static OkHttpClient getUnsafeOkHttpClient(Context context){try {final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {@Overridepublic void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {}@Overridepublic void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {}@Overridepublic 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() {@Overridepublic 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;}@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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);}}@Overridepublic 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 herePicasso.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() {@Overridepublic 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 ResultOfMethodCallIgnoredcache.mkdirs();}return cache;}@Overridepublic 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);}}@Overridepublic 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> {@Overrideprotected 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

tinode客户端安卓版编译手账相关推荐

  1. 手机数控模拟器安卓版_数控机床模拟器手机版下载-数控机床模拟器高级版下载v1.1.4 安卓版-单机手游网...

    数控机床模拟器安卓版是一款专业的机床模拟器手机软件.你可以通过他设计出各种东西,考验着玩家的想象力,喜欢这类题材游戏的玩家还在犹豫什么,快来单机100手游网下载体验吧! 数控机床模拟器手机版简介 &l ...

  2. android内存最小版本下载,猫和老鼠精简版下载-猫和老鼠内存最小版下载v6.6.1 安卓版-芒果手游网...

    猫和老鼠内存最小版同名动漫相信大家都非常熟悉因为它曾带给我们充满欢笑的童年,现如今推出原滋原味精简版猫和老鼠手游来让我们重温儿时美好的回忆,喜欢的朋友快来点击下载吧. <猫和老鼠>全新1v ...

  3. daysmatter安卓版_days matter最新版下载-Days Matter倒数日软件下载v2.2.1 安卓版-单机手游网...

    days matter倒数日软件上面是可以记录一些重要的东西的,比如说生日.还有一些比较重要的会议等,然后到时间后就会提醒你,这样你就不会忘记了啊,还挺好用的,简单有效的时间管理软件. days ma ...

  4. 暗棋单机版_中国暗棋游戏下载-中国暗棋下载v1.0.0 安卓版-单机手游网

    中国暗棋游戏去棋牌类象棋手游,游戏玩法是将象棋到放,然后不断的反面获得游戏的进程,玩法市面简单方便可以和线上线下朋友一起玩耍哦!小编也给你们带来了中国暗棋手游下载地址!此版本为安卓最新完整版,感兴趣的 ...

  5. 手游无限多开器安卓版_手游多开器安卓版-手游多开器手机版 _5577安卓网

    手游多开器手机版下载,本站为大家带来的是一款能够帮助大家多开手游的应用app,有需要的亲们就来下载可以同时登陆多个账号,满足大家在游戏中的相关需求,喜欢的亲们就不要错过啦! [温馨提示] 目前提供的是 ...

  6. dos模拟器即时存档工具_安卓dos模拟器下载-dos模拟器中文版下载v1.0.60 安卓版-2265手游网...

    dos模拟器中文版是一款非常棒的游戏模拟器软件.超级简单的命令使用操作,支持多种系统的管理:让你畅玩各种fc游戏,轻松自由的进行控制!你还在等什么?快来2265安卓网下载吧! 安卓dos模拟器软件介绍 ...

  7. 快手小筷子机器人_小筷子app官方版下载-快手控场机器人小筷子app下载v1.0.0安卓版_289手游网...

    快手控场机器人小筷子app是一个专门为快手主播打造的专业实用工具,是每个快手主播的最佳直播伴侣!快手控场机器人小筷子app能够实时与观众弹幕互动,还能语音播报各种礼物答谢等等,帮助每个主播更好的聚拢粉 ...

  8. 华为手机助手安卓版_lol手游助手IOS版手机下载_lol手游助手安卓版下载

    lol手游助手IOS版是一款非常给力的英雄联盟手游辅助软件,这款lol手游助手IOS版有着非常强大的功能,安装方法非常的简单,上手也很轻松,感兴趣的玩家赶快下载来试一试吧! lol手游助手IOS版游戏 ...

  9. 侠义java加速_侠义加速版下载-侠义加速版游戏下载v1.7 安卓版-单机手游网

    侠义加速版是一款经典的复古武侠手游,游戏完美还原了一个武侠世界该有的一切东西,带你体验武侠江湖的恩怨情仇,丰富的玩法模式让你爱不释手,全方位的战力提升系统助你突破巅峰.喜欢这款游戏的小伙伴们快来单机1 ...

最新文章

  1. JSONObject 和 JSONArray 获取value 的方法
  2. ES6深入学习记录(一)class方法相关
  3. 一个项目中既有移动端,同时也有PC端的代码,并且 他们的代码分开写的,那么如何实现在手机跳转手机页面,pc点击跳转pc页面...
  4. aproxy配合Nginx搭建Web集群部署实验(图文详解)
  5. [uoj24]缩紧优化
  6. Spring缓存注解@Cache使用
  7. fate框架找不到 flow 命令
  8. 阿里CTO谈BAT:李彦宏说是新瓶装旧酒、马化腾说太遥远了、马云说今天就应该做
  9. Git与GitHub学习笔记(一)如何删除github里面的文件夹?
  10. Movie播放Gif,完美实现屏幕适配
  11. 这一小点程序员务实的调整,可以避免整个开发团队的崩溃
  12. 2021美赛C题解题记录(内含完整代码)
  13. operator重载
  14. 手机拍照反差对焦、相位对焦和激光对焦系统解析
  15. 国庆弯道超车正当时,推荐一个免费的刷题网站。。。Python小伙伴可以看过来了
  16. 怎样给蔬菜图片抠图?看看这个马路中央的紫茄子
  17. 【pyqt5】实现选择文件界面
  18. 亚信科技笔试java
  19. 一篇好文,以在迷茫时品味…………
  20. IIS服务器(Windows)远程修改ftp密码方法

热门文章

  1. Think-swoole的使用
  2. 大厂社区、知名技术博主,学习连接看这里,全!!!
  3. 天猫爬虫--爬取天猫评论中的图片
  4. ElasticSearch健康检查localhost:9200 not reachable
  5. Excel办公中的应用(中秋快乐学习)(注意:全部为英文符号)
  6. ImportError: No module named osgeo解决办法
  7. java小白日常报错
  8. 开心一刻:邪恶的小明
  9. 云e办学习笔记(四)SpringSecurity学习(二)
  10. 微信8年,这几个冷知识你都知道吗?想必知道的人也不多吧