diff --git a/app/build.gradle b/app/build.gradle
index b05e88672..86512389a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -38,6 +38,7 @@ dependencies {
compile 'com.mikhaellopez:circularfillableloaders:1.2.0'
compile 'com.squareup.retrofit2:retrofit:2.2.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
+ compile 'com.squareup.okhttp3:logging-interceptor:3.6.0'
compile 'com.github.chrisbanes:PhotoView:1.3.1'
compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar'
compile 'com.github.arimorty:floatingsearchview:2.0.3'
diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
index e34744c47..a83bb9e05 100644
--- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
@@ -34,7 +34,10 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import okhttp3.ConnectionSpec;
import okhttp3.Dispatcher;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
@@ -114,32 +117,13 @@ public class BaseActivity extends AppCompatActivity {
protected void createMastodonAPI() {
mastodonApiDispatcher = new Dispatcher();
- OkHttpClient okHttpClient = new OkHttpClient.Builder()
- .addInterceptor(new Interceptor() {
- @Override
- public Response intercept(Chain chain) throws IOException {
- Request originalRequest = chain.request();
-
- Request.Builder builder = originalRequest.newBuilder();
- String accessToken = getAccessToken();
- if (accessToken != null) {
- builder.header("Authorization", String.format("Bearer %s", accessToken));
- }
- Request newRequest = builder.build();
-
- return chain.proceed(newRequest);
- }
- })
- .dispatcher(mastodonApiDispatcher)
- .build();
-
Gson gson = new GsonBuilder()
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getBaseUrl())
- .client(okHttpClient)
+ .client(OkHttpUtils.getCompatibleClient())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
@@ -149,6 +133,7 @@ public class BaseActivity extends AppCompatActivity {
protected void createTuskyAPI() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getString(R.string.tusky_api_url))
+ .client(OkHttpUtils.getCompatibleClient())
.build();
tuskyAPI = retrofit.create(TuskyAPI.class);
diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java
index 11ede95b7..29712d0f5 100644
--- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java
@@ -117,6 +117,7 @@ public class LoginActivity extends AppCompatActivity {
private MastodonAPI getApiFor(String domain) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + domain)
+ .client(OkHttpUtils.getCompatibleClient())
.addConverterFactory(GsonConverterFactory.create())
.build();
@@ -168,8 +169,10 @@ public class LoginActivity extends AppCompatActivity {
};
try {
- getApiFor(domain).authenticateApp(getString(R.string.app_name), getOauthRedirectUri(), OAUTH_SCOPES,
- getString(R.string.app_website)).enqueue(callback);
+ getApiFor(domain)
+ .authenticateApp(getString(R.string.app_name), getOauthRedirectUri(),
+ OAUTH_SCOPES, getString(R.string.app_website))
+ .enqueue(callback);
} catch (IllegalArgumentException e) {
editText.setError(getString(R.string.error_invalid_domain));
}
@@ -234,7 +237,7 @@ public class LoginActivity extends AppCompatActivity {
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "The app version was not found. " + e.getMessage());
}
- if (preferences.getInt("lastUpdate", 0) != versionCode) {
+ if (preferences.getInt("lastUpdateVersion", 0) != versionCode) {
SharedPreferences.Editor editor = preferences.edit();
if (versionCode == 14) {
/* This version switches the order of scheme and host in the OAuth redirect URI.
@@ -243,7 +246,7 @@ public class LoginActivity extends AppCompatActivity {
* "rememberedVisibility", "loggedInUsername", and "loggedInAccountId". */
editor.clear();
}
- editor.putInt("lastUpdate", versionCode);
+ editor.putInt("lastUpdateVersion", versionCode);
editor.apply();
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java b/app/src/main/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java
index ec3300618..4e963d684 100644
--- a/app/src/main/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java
+++ b/app/src/main/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java
@@ -20,6 +20,7 @@ public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService {
protected void createTuskyAPI() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getString(R.string.tusky_api_url))
+ .client(OkHttpUtils.getCompatibleClient())
.build();
tuskyAPI = retrofit.create(TuskyAPI.class);
diff --git a/app/src/main/java/com/keylesspalace/tusky/MyFirebaseMessagingService.java b/app/src/main/java/com/keylesspalace/tusky/MyFirebaseMessagingService.java
index 0031080ce..2ef29e37d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/MyFirebaseMessagingService.java
+++ b/app/src/main/java/com/keylesspalace/tusky/MyFirebaseMessagingService.java
@@ -84,7 +84,7 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
final String domain = preferences.getString("domain", null);
final String accessToken = preferences.getString("accessToken", null);
- OkHttpClient okHttpClient = new OkHttpClient.Builder()
+ OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
.addInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
diff --git a/app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java b/app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java
new file mode 100644
index 000000000..bb25b7161
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java
@@ -0,0 +1,157 @@
+/* Copyright 2017 Andrew Dawson
+ *
+ * This file is part of Tusky.
+ *
+ * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * Lesser General Public License as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with Tusky. If
+ * not, see . */
+
+package com.keylesspalace.tusky;
+
+import android.os.Build;
+import android.support.annotation.NonNull;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import okhttp3.ConnectionSpec;
+import okhttp3.OkHttpClient;
+import okhttp3.logging.HttpLoggingInterceptor;
+
+class OkHttpUtils {
+ private static final String TAG = "OkHttpUtils"; // logging tag
+
+ @NonNull
+ static OkHttpClient.Builder getCompatibleClientBuilder() {
+ ConnectionSpec fallback = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .allEnabledCipherSuites()
+ .supportsTlsExtensions(true)
+ .build();
+
+ List specList = new ArrayList<>();
+ specList.add(ConnectionSpec.MODERN_TLS);
+ specList.add(fallback);
+ specList.add(ConnectionSpec.CLEARTEXT);
+
+ HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
+ loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
+
+ OkHttpClient.Builder builder = new OkHttpClient.Builder()
+ .connectionSpecs(specList)
+ .addInterceptor(loggingInterceptor);
+
+ return enableHigherTlsOnPreLollipop(builder);
+ }
+
+ @NonNull
+ static OkHttpClient getCompatibleClient() {
+ return getCompatibleClientBuilder().build();
+ }
+
+ private static OkHttpClient.Builder enableHigherTlsOnPreLollipop(OkHttpClient.Builder builder) {
+ if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT < 22) {
+ try {
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init((KeyStore) null);
+ TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+ if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
+ throw new IllegalStateException("Unexpected default trust managers:"
+ + Arrays.toString(trustManagers));
+ }
+
+ X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
+
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, new TrustManager[] { trustManager }, null);
+ SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
+
+ builder.sslSocketFactory(new Tls11And12SocketFactory(sslSocketFactory),
+ trustManager);
+ } catch (NoSuchAlgorithmException|KeyStoreException|KeyManagementException e) {
+ Log.e(TAG, "Failed enabling TLS 1.1 & 1.2. " + e.getMessage());
+ }
+ }
+
+ return builder;
+ }
+
+ private static class Tls11And12SocketFactory extends SSLSocketFactory {
+ private static final String[] TLS_VERSIONS = { "TLSv1.1", "TLSv1.2" };
+
+ final SSLSocketFactory delegate;
+
+ Tls11And12SocketFactory(SSLSocketFactory base) {
+ this.delegate = base;
+ }
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return delegate.getDefaultCipherSuites();
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return delegate.getSupportedCipherSuites();
+ }
+
+ @Override
+ public Socket createSocket(Socket s, String host, int port, boolean autoClose)
+ throws IOException {
+ return patch(delegate.createSocket(s, host, port, autoClose));
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
+ return patch(delegate.createSocket(host, port));
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
+ throws IOException, UnknownHostException {
+ return patch(delegate.createSocket(host, port, localHost, localPort));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ return patch(delegate.createSocket(host, port));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port, InetAddress localAddress,
+ int localPort) throws IOException {
+ return patch(delegate.createSocket(address, port, localAddress, localPort));
+ }
+
+ private Socket patch(Socket socket) {
+ if (socket instanceof SSLSocket) {
+ SSLSocket sslSocket = (SSLSocket) socket;
+ sslSocket.setEnabledProtocols(TLS_VERSIONS);
+ }
+ return socket;
+ }
+ }
+}