tinode客户端安卓版编译手账
前一陣子我自己架設(shè)了一個tinode的IM服務(wù)器,
web直接可以運(yùn)行
但是安卓版本的一直報(bào)錯,
具體信息為:
No subjectAltNames on the certificate match問了作者,作者竟然把我的問題直接刪除了,還是自己調(diào)試代碼吧。畢竟源碼面前,了無秘密;
一、代碼地址
GitHub - tinode/tindroid: Tinode chat client application for Android
我從release部分下載了0.20.9版本源碼
二、更改源碼配置
1)根目錄下的build.gradle有2處需要更改,主要是版本信息,非git版本無從提取,隨便設(shè)置一下
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,需要去官網(wǎng)注冊一下相關(guān)的資料,自己注冊一個新的應(yīng)用,下載得到google-services.json,這個文件放置于app目錄;
2.2)google-services.json中我們注冊了一個應(yīng)用的名字,這文件中有個package_name替換原來的應(yīng)用ID,否則編譯不過
applicationId "com.birdschat.cn"2.3)創(chuàng)建證書,文件放置于源碼同級目錄,比如我的:
../robinkeys/key.keystore在根目錄下添加一個配置文件,叫keystore.properties,內(nèi)容大概如下:
keystoreFile=../robin_keys/key.keystore keystoreAlias=key.keystore keystorePassword=123456 keyPassword=123456并根據(jù)自己配置文件中的參數(shù)名,設(shè)置一下build.gradle:
signingConfigs {release {storeFile file(keystoreProperties['keystoreFile'])storePassword keystoreProperties['keystorePassword']keyAlias keystoreProperties['keyAlias']keyPassword keystoreProperties['keyPassword']}}這樣應(yīng)該就可以編譯了!!
3)取消客戶端WebSocket 的SSL雙向認(rèn)證
但是運(yùn)行后,設(shè)置了自己的服務(wù)器,以及使用加密模式,無法注冊或者登錄,
主要是我們的證書需要有域名,并且是申請來的,也就是有CA認(rèn)證的,而不是自己生成的,不然無法實(shí)現(xiàn)雙向驗(yàn)證,這主要是為了防止中間人攻擊;
但是我們往往就是自己內(nèi)部試用,不需要這么麻煩,
需要對SDK部分代碼進(jìn)行更該,參考: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)設(shè)置服務(wù)器默認(rèn)參數(shù)
將服務(wù)器的鏈接參數(shù)預(yù)先設(shè)置好為我們需要的:
4.1) 地址與端口:全文搜索“:6060”字樣,在資源文件res/strings.xml中更改:
<string name="emulator_host_name" translatable="false">119.0.0.1:6060</string>同時,將build.gradle的相關(guān)位置做更改,自動生成相關(guān)的資源文件
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)默認(rèn)使用https,更改TindroidApp.java
將返回的默認(rèn)的參數(shù)設(shè)置為true
public static boolean getDefaultTLS() {//return !isEmulator();return true;}編譯好了就可以用了!
5) 還需要更改LargeFileHelper,
在完成第4步驟后,發(fā)送小文件正常,大文件比如5兆,就報(bào)錯了,?
java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.這說明大文件使用其他的方法發(fā)送時驗(yàn)證證書失敗了,果然,SDK中使用了單獨(dú)的一個輔助類單獨(dú)開了一個鏈接POST發(fā)送大文件,參考如下鏈接: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;} }編譯后,發(fā)送大文件不再報(bào)錯了。
6)還需要發(fā)送圖片時,無法正常彈出圖片瀏覽器框
調(diào)試過程中發(fā)現(xiàn),是由于我們更改了應(yīng)用程序的ID,造成默認(rèn)的路徑發(fā)生了變化,在讀取臨時文件時候發(fā)生了錯誤,所以需要更改一下相關(guān)的路徑: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>這里應(yīng)該與applicationId中設(shè)置的一樣才行。
其實(shí)這里的錯誤,經(jīng)過分析代碼,是因?yàn)镸essagesFragment.java 在試圖發(fā)送圖片時候,程序會嘗試新建一個文件用于保存照片,所以才會使用了臨時文件,而這里由于邏輯問題,在真機(jī)上每次即使不拍照也會產(chǎn)生垃圾數(shù)據(jù),所以決定禁用了拍照,選擇時候反而更加流程;
7)聊天頭像丟失的問題
這個其實(shí)也是認(rèn)證的問題,主要是在ChatsAdapter.java中bing函數(shù)設(shè)置avatar時候,使用了piccaso來下載對應(yīng)的圖片,但是不能直接改;因?yàn)樵赥indroidApp.java中初始化時候設(shè)置了Okhttp3下載工具,并且設(shè)置了相對鏈接的轉(zhuǎn)換方式,
所以應(yīng)該改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;}} }頭像已經(jīng)可以出現(xiàn)了,
?8) 還剩下一個問題,就是有時候某些圖片發(fā)送好像失敗了,
待查
結(jié)束。
備注:編譯好的apk?https://download.csdn.net/download/robinfoxnan/87300700
總結(jié)
以上是生活随笔為你收集整理的tinode客户端安卓版编译手账的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 复购率/回购率/新购人数
- 下一篇: android 屏幕统计,OffScre
