diff --git a/app/src/main/java/com/github/gotify/SSLSettings.java b/app/src/main/java/com/github/gotify/SSLSettings.java new file mode 100644 index 0000000..cb8c4a6 --- /dev/null +++ b/app/src/main/java/com/github/gotify/SSLSettings.java @@ -0,0 +1,11 @@ +package com.github.gotify; + +public class SSLSettings { + public boolean validateSSL; + public String cert; + + public SSLSettings(boolean validateSSL, String cert) { + this.validateSSL = validateSSL; + this.cert = cert; + } +} diff --git a/app/src/main/java/com/github/gotify/Settings.java b/app/src/main/java/com/github/gotify/Settings.java index 86726c8..14139ff 100644 --- a/app/src/main/java/com/github/gotify/Settings.java +++ b/app/src/main/java/com/github/gotify/Settings.java @@ -34,6 +34,8 @@ public class Settings { public void clear() { url(null); token(null); + validateSSL(true); + cert(null); } public void user(String name, boolean admin) { @@ -57,4 +59,24 @@ public class Settings { public void serverVersion(String version) { sharedPreferences.edit().putString("version", version).apply(); } + + private boolean validateSSL() { + return sharedPreferences.getBoolean("validateSSL", true); + } + + public void validateSSL(boolean validateSSL) { + sharedPreferences.edit().putBoolean("validateSSL", validateSSL).apply(); + } + + private String cert() { + return sharedPreferences.getString("cert", null); + } + + public void cert(String cert) { + sharedPreferences.edit().putString("cert", cert).apply(); + } + + public SSLSettings sslSettings() { + return new SSLSettings(validateSSL(), cert()); + } } diff --git a/app/src/main/java/com/github/gotify/Utils.java b/app/src/main/java/com/github/gotify/Utils.java index 7cc8e1f..927e5f7 100644 --- a/app/src/main/java/com/github/gotify/Utils.java +++ b/app/src/main/java/com/github/gotify/Utils.java @@ -7,12 +7,18 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.text.format.DateUtils; import android.view.View; +import androidx.annotation.NonNull; import com.github.gotify.client.ApiClient; import com.github.gotify.client.JSON; import com.github.gotify.log.Log; import com.google.android.material.snackbar.Snackbar; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import okio.Buffer; import org.threeten.bp.OffsetDateTime; public class Utils { @@ -49,7 +55,27 @@ public class Utils { return new ApiClient().getJSON(); } + public static String readFileFromStream(@NonNull InputStream inputStream) { + StringBuilder sb = new StringBuilder(); + String currentLine; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + while ((currentLine = reader.readLine()) != null) { + sb.append(currentLine).append("\n"); + } + } catch (IOException e) { + throw new IllegalArgumentException("failed to read input"); + } + + return sb.toString(); + } + public interface DrawableReceiver { void loaded(Drawable drawable); } + + public static InputStream stringToInputStream(String str) { + if (str == null) return null; + return new Buffer().writeUtf8(str).inputStream(); + } } diff --git a/app/src/main/java/com/github/gotify/api/CertUtils.java b/app/src/main/java/com/github/gotify/api/CertUtils.java new file mode 100644 index 0000000..0ca7010 --- /dev/null +++ b/app/src/main/java/com/github/gotify/api/CertUtils.java @@ -0,0 +1,106 @@ +package com.github.gotify.api; + +import android.annotation.SuppressLint; +import com.github.gotify.SSLSettings; +import com.github.gotify.Utils; +import com.github.gotify.log.Log; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import okhttp3.OkHttpClient; + +public class CertUtils { + private static final X509TrustManager trustAll = + new X509TrustManager() { + @SuppressLint("TrustAllX509TrustManager") + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + + @SuppressLint("TrustAllX509TrustManager") + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[] {}; + } + }; + + public static Certificate parseCertificate(String cert) { + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); + + return certificateFactory.generateCertificate(Utils.stringToInputStream(cert)); + } catch (Exception e) { + throw new IllegalArgumentException("certificate is invalid"); + } + } + + public static void applySslSettings(OkHttpClient.Builder builder, SSLSettings settings) { + // Modified from ApiClient.applySslSettings in the client package. + + try { + if (!settings.validateSSL) { + SSLContext context = SSLContext.getInstance("TLS"); + context.init( + new KeyManager[] {}, new TrustManager[] {trustAll}, new SecureRandom()); + builder.sslSocketFactory(context.getSocketFactory(), trustAll); + builder.hostnameVerifier((a, b) -> true); + return; + } + + if (settings.cert != null) { + TrustManager[] trustManagers = certToTrustManager(settings.cert); + + if (trustManagers != null && trustManagers.length > 0) { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(new KeyManager[] {}, trustManagers, new SecureRandom()); + builder.sslSocketFactory( + context.getSocketFactory(), (X509TrustManager) trustManagers[0]); + } + } + } catch (Exception e) { + // We shouldn't have issues since the cert is verified on login. + Log.e("Failed to apply SSL settings", e); + } + } + + private static TrustManager[] certToTrustManager(String cert) throws GeneralSecurityException { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + Collection certificates = + certificateFactory.generateCertificates(Utils.stringToInputStream(cert)); + if (certificates.isEmpty()) { + throw new IllegalArgumentException("expected non-empty set of trusted certificates"); + } + KeyStore caKeyStore = newEmptyKeyStore(); + int index = 0; + for (Certificate certificate : certificates) { + String certificateAlias = "ca" + Integer.toString(index++); + caKeyStore.setCertificateEntry(certificateAlias, certificate); + } + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(caKeyStore); + return trustManagerFactory.getTrustManagers(); + } + + private static KeyStore newEmptyKeyStore() throws GeneralSecurityException { + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + return keyStore; + } catch (IOException e) { + throw new AssertionError(e); + } + } +} diff --git a/app/src/main/java/com/github/gotify/api/ClientFactory.java b/app/src/main/java/com/github/gotify/api/ClientFactory.java index e0e65eb..beaaf94 100644 --- a/app/src/main/java/com/github/gotify/api/ClientFactory.java +++ b/app/src/main/java/com/github/gotify/api/ClientFactory.java @@ -1,6 +1,8 @@ package com.github.gotify.api; +import com.github.gotify.SSLSettings; import com.github.gotify.Settings; +import com.github.gotify.Utils; import com.github.gotify.client.ApiClient; import com.github.gotify.client.api.UserApi; import com.github.gotify.client.api.VersionApi; @@ -8,14 +10,17 @@ import com.github.gotify.client.auth.ApiKeyAuth; import com.github.gotify.client.auth.HttpBasicAuth; public class ClientFactory { - public static ApiClient unauthorized(String baseUrl) { + public static ApiClient unauthorized(String baseUrl, SSLSettings sslSettings) { ApiClient client = new ApiClient(); + client.setVerifyingSsl(sslSettings.validateSSL); + client.setSslCaCert(Utils.stringToInputStream(sslSettings.cert)); client.setBasePath(baseUrl); return client; } - public static ApiClient basicAuth(String baseUrl, String username, String password) { - ApiClient client = unauthorized(baseUrl); + public static ApiClient basicAuth( + String baseUrl, SSLSettings sslSettings, String username, String password) { + ApiClient client = unauthorized(baseUrl, sslSettings); HttpBasicAuth auth = (HttpBasicAuth) client.getAuthentication("basicAuth"); auth.setUsername(username); auth.setPassword(password); @@ -23,18 +28,18 @@ public class ClientFactory { return client; } - public static ApiClient clientToken(String baseUrl, String token) { - ApiClient client = unauthorized(baseUrl); + public static ApiClient clientToken(String baseUrl, SSLSettings sslSettings, String token) { + ApiClient client = unauthorized(baseUrl, sslSettings); ApiKeyAuth tokenAuth = (ApiKeyAuth) client.getAuthentication("clientTokenHeader"); tokenAuth.setApiKey(token); return client; } - public static VersionApi versionApi(String baseUrl) { - return new VersionApi(unauthorized(baseUrl)); + public static VersionApi versionApi(String baseUrl, SSLSettings sslSettings) { + return new VersionApi(unauthorized(baseUrl, sslSettings)); } public static UserApi userApiWithToken(Settings settings) { - return new UserApi(clientToken(settings.url(), settings.token())); + return new UserApi(clientToken(settings.url(), settings.sslSettings(), settings.token())); } } diff --git a/app/src/main/java/com/github/gotify/init/InitializationActivity.java b/app/src/main/java/com/github/gotify/init/InitializationActivity.java index f69f5ad..70574c4 100644 --- a/app/src/main/java/com/github/gotify/init/InitializationActivity.java +++ b/app/src/main/java/com/github/gotify/init/InitializationActivity.java @@ -116,7 +116,7 @@ public class InitializationActivity extends AppCompatActivity { private void requestVersion( final Callback.SuccessCallback callback, final Callback.ErrorCallback errorCallback) { - VersionApi versionApi = ClientFactory.versionApi(settings.url()); + VersionApi versionApi = ClientFactory.versionApi(settings.url(), settings.sslSettings()); Api.withLogging(versionApi::getVersionAsync) .handleInUIThread(this, callback, errorCallback); } diff --git a/app/src/main/java/com/github/gotify/login/AdvancedDialog.java b/app/src/main/java/com/github/gotify/login/AdvancedDialog.java new file mode 100644 index 0000000..f52fd1e --- /dev/null +++ b/app/src/main/java/com/github/gotify/login/AdvancedDialog.java @@ -0,0 +1,96 @@ +package com.github.gotify.login; + +import android.app.AlertDialog; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; +import androidx.annotation.Nullable; +import butterknife.BindView; +import butterknife.ButterKnife; +import com.github.gotify.R; + +class AdvancedDialog { + + private Context context; + private ViewHolder holder; + private CompoundButton.OnCheckedChangeListener onCheckedChangeListener; + private Runnable onClickSelectCaCertificate; + private Runnable onClickRemoveCaCertificate; + + AdvancedDialog(Context context) { + this.context = context; + } + + AdvancedDialog onDisableSSLChanged( + CompoundButton.OnCheckedChangeListener onCheckedChangeListener) { + this.onCheckedChangeListener = onCheckedChangeListener; + return this; + } + + AdvancedDialog onClickSelectCaCertificate(Runnable onClickSelectCaCertificate) { + this.onClickSelectCaCertificate = onClickSelectCaCertificate; + return this; + } + + AdvancedDialog onClickRemoveCaCertificate(Runnable onClickRemoveCaCertificate) { + this.onClickRemoveCaCertificate = onClickRemoveCaCertificate; + return this; + } + + AdvancedDialog show(boolean disableSSL, @Nullable String selectedCertificate) { + + View dialogView = + LayoutInflater.from(context).inflate(R.layout.advanced_settings_dialog, null); + holder = new ViewHolder(dialogView); + holder.disableSSL.setChecked(disableSSL); + holder.disableSSL.setOnCheckedChangeListener(onCheckedChangeListener); + + if (selectedCertificate == null) { + showSelectCACertificate(); + } else { + showRemoveCACertificate(selectedCertificate); + } + + new AlertDialog.Builder(context) + .setView(dialogView) + .setTitle(R.string.advanced_settings) + .setPositiveButton(context.getString(R.string.done), (ignored, ignored2) -> {}) + .show(); + return this; + } + + private void showSelectCACertificate() { + holder.toggleCaCert.setText(R.string.select_ca_certificate); + holder.toggleCaCert.setOnClickListener((a) -> onClickSelectCaCertificate.run()); + holder.selectedCaCertificate.setText(R.string.no_certificate_selected); + } + + void showRemoveCACertificate(String certificate) { + holder.toggleCaCert.setText(R.string.remove_ca_certificate); + holder.toggleCaCert.setOnClickListener( + (a) -> { + showSelectCACertificate(); + onClickRemoveCaCertificate.run(); + }); + holder.selectedCaCertificate.setText(certificate); + } + + class ViewHolder { + @BindView(R.id.disableSSL) + CheckBox disableSSL; + + @BindView(R.id.toggle_ca_cert) + Button toggleCaCert; + + @BindView(R.id.seleceted_ca_cert) + TextView selectedCaCertificate; + + ViewHolder(View view) { + ButterKnife.bind(this, view); + } + } +} diff --git a/app/src/main/java/com/github/gotify/login/LoginActivity.java b/app/src/main/java/com/github/gotify/login/LoginActivity.java index ad866e3..2fce438 100644 --- a/app/src/main/java/com/github/gotify/login/LoginActivity.java +++ b/app/src/main/java/com/github/gotify/login/LoginActivity.java @@ -1,13 +1,17 @@ package com.github.gotify.login; +import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; +import android.widget.ImageView; import android.widget.ProgressBar; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import butterknife.BindView; @@ -15,10 +19,12 @@ import butterknife.ButterKnife; import butterknife.OnClick; import butterknife.OnTextChanged; import com.github.gotify.R; +import com.github.gotify.SSLSettings; import com.github.gotify.Settings; import com.github.gotify.Utils; import com.github.gotify.api.Api; import com.github.gotify.api.Callback; +import com.github.gotify.api.CertUtils; import com.github.gotify.api.ClientFactory; import com.github.gotify.client.ApiClient; import com.github.gotify.client.ApiException; @@ -30,9 +36,15 @@ import com.github.gotify.init.InitializationActivity; import com.github.gotify.log.Log; import com.github.gotify.log.UncaughtExceptionHandler; import com.squareup.okhttp.HttpUrl; +import java.io.InputStream; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; public class LoginActivity extends AppCompatActivity { + // return value from startActivityForResult when choosing a certificate + private final int FILE_SELECT_CODE = 1; + @BindView(R.id.username) EditText usernameField; @@ -42,6 +54,9 @@ public class LoginActivity extends AppCompatActivity { @BindView(R.id.password) EditText passwordField; + @BindView(R.id.advanced_settings) + ImageView toggleAdvanced; + @BindView(R.id.checkurl) Button checkUrlButton; @@ -56,6 +71,10 @@ public class LoginActivity extends AppCompatActivity { private Settings settings; + private boolean disableSSLValidation; + private String caCertContents; + private AdvancedDialog advancedDialog; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -68,9 +87,14 @@ public class LoginActivity extends AppCompatActivity { @OnTextChanged(R.id.gotify_url) public void onUrlChange() { - usernameField.setVisibility(View.INVISIBLE); - passwordField.setVisibility(View.INVISIBLE); - loginButton.setVisibility(View.INVISIBLE); + invalidateUrl(); + } + + private void invalidateUrl() { + usernameField.setVisibility(View.GONE); + passwordField.setVisibility(View.GONE); + loginButton.setVisibility(View.GONE); + checkUrlButton.setText(getString(R.string.check_url)); } @OnClick(R.id.checkurl) @@ -82,18 +106,102 @@ public class LoginActivity extends AppCompatActivity { } checkUrlProgress.setVisibility(View.VISIBLE); - checkUrlButton.setVisibility(View.INVISIBLE); + checkUrlButton.setVisibility(View.GONE); final String fixedUrl = url.endsWith("/") ? url.substring(0, url.length() - 1) : url; - Api.withLogging(ClientFactory.versionApi(fixedUrl)::getVersionAsync) + Api.withLogging( + ClientFactory.versionApi( + fixedUrl, + new SSLSettings(!disableSSLValidation, caCertContents)) + ::getVersionAsync) .handleInUIThread(this, onValidUrl(fixedUrl), onInvalidUrl(fixedUrl)); } + @OnClick(R.id.advanced_settings) + void toggleShowAdvanced() { + String selectedCertName = + caCertContents != null ? getNameOfCertContent(caCertContents) : null; + + advancedDialog = + new AdvancedDialog(this) + .onDisableSSLChanged( + (ignored, disable) -> { + invalidateUrl(); + disableSSLValidation = disable; + }) + .onClickSelectCaCertificate( + () -> { + invalidateUrl(); + doSelectCACertificate(); + }) + .onClickRemoveCaCertificate( + () -> { + invalidateUrl(); + caCertContents = null; + }) + .show(disableSSLValidation, selectedCertName); + } + + private void doSelectCACertificate() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + // we don't really care what kind of file it is as long as we can parse it + intent.setType("*/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + + try { + startActivityForResult( + Intent.createChooser(intent, getString(R.string.select_ca_file)), + FILE_SELECT_CODE); + } catch (ActivityNotFoundException e) { + // case for user not having a file browser installed + Utils.showSnackBar(LoginActivity.this, getString(R.string.please_install_file_browser)); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + try { + if (requestCode == FILE_SELECT_CODE) { + if (resultCode != RESULT_OK) { + throw new IllegalArgumentException(String.format("result was %d", resultCode)); + } else if (data == null) { + throw new IllegalArgumentException("file path was null"); + } + + Uri uri = data.getData(); + if (uri == null) { + throw new IllegalArgumentException("file path was null"); + } + + InputStream fileStream = getContentResolver().openInputStream(uri); + if (fileStream == null) { + throw new IllegalArgumentException("file path was invalid"); + } + + String content = Utils.readFileFromStream(fileStream); + String name = getNameOfCertContent(content); + + // temporarily set the contents (don't store to settings until they decide to login) + caCertContents = content; + advancedDialog.showRemoveCACertificate(name); + } + } catch (Exception e) { + Utils.showSnackBar( + LoginActivity.this, getString(R.string.select_ca_failed, e.getMessage())); + } + } + + private String getNameOfCertContent(String content) { + Certificate ca = CertUtils.parseCertificate(content); + return ((X509Certificate) ca).getSubjectDN().getName(); + } + private Callback.SuccessCallback onValidUrl(String url) { return (version) -> { settings.url(url); - checkUrlProgress.setVisibility(View.INVISIBLE); + checkUrlProgress.setVisibility(View.GONE); checkUrlButton.setVisibility(View.VISIBLE); checkUrlButton.setText(getString(R.string.found_gotify_version, version.getVersion())); usernameField.setVisibility(View.VISIBLE); @@ -105,7 +213,7 @@ public class LoginActivity extends AppCompatActivity { private Callback.ErrorCallback onInvalidUrl(String url) { return (exception) -> { - checkUrlProgress.setVisibility(View.INVISIBLE); + checkUrlProgress.setVisibility(View.GONE); checkUrlButton.setVisibility(View.VISIBLE); Utils.showSnackBar(LoginActivity.this, versionError(url, exception)); }; @@ -116,17 +224,22 @@ public class LoginActivity extends AppCompatActivity { String username = usernameField.getText().toString(); String password = passwordField.getText().toString(); - loginButton.setVisibility(View.INVISIBLE); + loginButton.setVisibility(View.GONE); loginProgress.setVisibility(View.VISIBLE); - ApiClient client = ClientFactory.basicAuth(settings.url(), username, password); + ApiClient client = + ClientFactory.basicAuth( + settings.url(), + new SSLSettings(!disableSSLValidation, caCertContents), + username, + password); Api.withLogging(new UserApi(client)::currentUserAsync) .handleInUIThread(this, (user) -> newClientDialog(client), this::onInvalidLogin); } private void onInvalidLogin(ApiException e) { loginButton.setVisibility(View.VISIBLE); - loginProgress.setVisibility(View.INVISIBLE); + loginProgress.setVisibility(View.GONE); Utils.showSnackBar(this, getString(R.string.wronguserpw)); } @@ -153,6 +266,9 @@ public class LoginActivity extends AppCompatActivity { private void onCreatedClient(Client client) { settings.token(client.getToken()); + settings.validateSSL(!disableSSLValidation); + settings.cert(caCertContents); + Utils.showSnackBar(this, getString(R.string.created_client)); startActivity(new Intent(this, InitializationActivity.class)); finish(); @@ -160,12 +276,12 @@ public class LoginActivity extends AppCompatActivity { private void onFailedToCreateClient(ApiException e) { Utils.showSnackBar(this, getString(R.string.create_client_failed)); - loginProgress.setVisibility(View.INVISIBLE); + loginProgress.setVisibility(View.GONE); loginButton.setVisibility(View.VISIBLE); } private void onCancelClientDialog(DialogInterface dialog, int which) { - loginProgress.setVisibility(View.INVISIBLE); + loginProgress.setVisibility(View.GONE); loginButton.setVisibility(View.VISIBLE); } diff --git a/app/src/main/java/com/github/gotify/messages/ListMessageAdapter.java b/app/src/main/java/com/github/gotify/messages/ListMessageAdapter.java index 87c2934..5d1eef3 100644 --- a/app/src/main/java/com/github/gotify/messages/ListMessageAdapter.java +++ b/app/src/main/java/com/github/gotify/messages/ListMessageAdapter.java @@ -21,12 +21,15 @@ import java.util.List; public class ListMessageAdapter extends BaseAdapter { private Context content; + private Picasso picasso; private List items; private Delete delete; - ListMessageAdapter(Context context, List items, Delete delete) { + ListMessageAdapter( + Context context, Picasso picasso, List items, Delete delete) { super(); this.content = context; + this.picasso = picasso; this.items = items; this.delete = delete; } @@ -62,8 +65,7 @@ public class ListMessageAdapter extends BaseAdapter { final MessageWithImage message = items.get(position); holder.message.setText(message.message.getMessage()); holder.title.setText(message.message.getTitle()); - Picasso.get() - .load(message.image) + picasso.load(message.image) .error(R.drawable.ic_alarm) .placeholder(R.drawable.ic_placeholder) .into(holder.image); diff --git a/app/src/main/java/com/github/gotify/messages/MessagesActivity.java b/app/src/main/java/com/github/gotify/messages/MessagesActivity.java index dcbe10a..293bf66 100644 --- a/app/src/main/java/com/github/gotify/messages/MessagesActivity.java +++ b/app/src/main/java/com/github/gotify/messages/MessagesActivity.java @@ -27,6 +27,7 @@ import com.github.gotify.MissedMessageUtil; import com.github.gotify.R; import com.github.gotify.Settings; import com.github.gotify.Utils; +import com.github.gotify.api.CertUtils; import com.github.gotify.api.ClientFactory; import com.github.gotify.client.ApiClient; import com.github.gotify.client.ApiException; @@ -46,11 +47,13 @@ import com.github.gotify.messages.provider.MessageWithImage; import com.github.gotify.service.WebSocketService; import com.google.android.material.navigation.NavigationView; import com.squareup.okhttp.HttpUrl; +import com.squareup.picasso.OkHttp3Downloader; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import okhttp3.OkHttpClient; import static java.util.Collections.emptyList; @@ -95,6 +98,8 @@ public class MessagesActivity extends AppCompatActivity private boolean isLoadMore = false; private Integer selectAppIdOnDrawerClose = null; + private Picasso picasso; + // we need to keep the target references otherwise they get gc'ed before they can be called. @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") private final List targetReferences = new ArrayList<>(); @@ -107,7 +112,10 @@ public class MessagesActivity extends AppCompatActivity Log.i("Entering " + getClass().getSimpleName()); settings = new Settings(this); - client = ClientFactory.clientToken(settings.url(), settings.token()); + picasso = makePicasso(); + + client = + ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token()); appsHolder = new ApplicationHolder(this, client); appsHolder.onUpdate(() -> onUpdateApps(appsHolder.get())); appsHolder.request(); @@ -116,7 +124,7 @@ public class MessagesActivity extends AppCompatActivity messages = new MessageFacade(new MessageApi(client), appsHolder); messagesView.setOnScrollListener(this); - messagesView.setAdapter(new ListMessageAdapter(this, emptyList(), this::delete)); + messagesView.setAdapter(new ListMessageAdapter(this, picasso, emptyList(), this::delete)); swipeRefreshLayout.setOnRefreshListener(this::onRefresh); drawer.addDrawerListener( @@ -158,8 +166,7 @@ public class MessagesActivity extends AppCompatActivity item.setCheckable(true); Target t = Utils.toDrawable(getResources(), item::setIcon); targetReferences.add(t); - Picasso.get() - .load(app.getImage()) + picasso.load(app.getImage()) .error(R.drawable.ic_alarm) .placeholder(R.drawable.ic_placeholder) .resize(100, 100) @@ -167,6 +174,15 @@ public class MessagesActivity extends AppCompatActivity } } + private Picasso makePicasso() { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + CertUtils.applySslSettings(builder, settings.sslSettings()); + + OkHttp3Downloader downloader = new OkHttp3Downloader(builder.build()); + + return new Picasso.Builder(this).downloader(downloader).build(); + } + private void initDrawer() { setSupportActionBar(toolbar); navigationView.setItemIconTintList(null); @@ -264,6 +280,12 @@ public class MessagesActivity extends AppCompatActivity super.onPause(); } + @Override + protected void onDestroy() { + super.onDestroy(); + picasso.shutdown(); + } + @Override public void onScrollStateChanged(AbsListView view, int scrollState) {} @@ -401,7 +423,9 @@ public class MessagesActivity extends AppCompatActivity @Override protected Void doInBackground(Void... ignore) { TokenApi api = - new TokenApi(ClientFactory.clientToken(settings.url(), settings.token())); + new TokenApi( + ClientFactory.clientToken( + settings.url(), settings.sslSettings(), settings.token())); stopService(new Intent(MessagesActivity.this, WebSocketService.class)); try { List clients = api.getClients(); diff --git a/app/src/main/java/com/github/gotify/service/WebSocketConnection.java b/app/src/main/java/com/github/gotify/service/WebSocketConnection.java index 650156d..cb08541 100644 --- a/app/src/main/java/com/github/gotify/service/WebSocketConnection.java +++ b/app/src/main/java/com/github/gotify/service/WebSocketConnection.java @@ -1,8 +1,10 @@ package com.github.gotify.service; import android.os.Handler; +import com.github.gotify.SSLSettings; import com.github.gotify.Utils; import com.github.gotify.api.Callback; +import com.github.gotify.api.CertUtils; import com.github.gotify.client.JSON; import com.github.gotify.client.model.Message; import com.github.gotify.log.Log; @@ -15,15 +17,9 @@ import okhttp3.WebSocket; import okhttp3.WebSocketListener; public class WebSocketConnection { + private OkHttpClient client; private static final JSON gson = Utils.json(); - private final OkHttpClient client = - new OkHttpClient.Builder() - .readTimeout(0, TimeUnit.MILLISECONDS) - .pingInterval(1, TimeUnit.MINUTES) - .connectTimeout(10, TimeUnit.SECONDS) - .build(); - private final Handler handler = new Handler(); private int errorCount = 0; @@ -38,7 +34,16 @@ public class WebSocketConnection { private Runnable onReconnected; private boolean isClosed; - WebSocketConnection(String baseUrl, String token) { + WebSocketConnection(String baseUrl, SSLSettings settings, String token) { + OkHttpClient.Builder builder = + new OkHttpClient.Builder() + .readTimeout(0, TimeUnit.MILLISECONDS) + .pingInterval(1, TimeUnit.MINUTES) + .connectTimeout(10, TimeUnit.SECONDS); + CertUtils.applySslSettings(builder, settings); + + client = builder.build(); + this.baseUrl = baseUrl; this.token = token; } diff --git a/app/src/main/java/com/github/gotify/service/WebSocketService.java b/app/src/main/java/com/github/gotify/service/WebSocketService.java index 92426f3..5110e10 100644 --- a/app/src/main/java/com/github/gotify/service/WebSocketService.java +++ b/app/src/main/java/com/github/gotify/service/WebSocketService.java @@ -44,7 +44,9 @@ public class WebSocketService extends Service { super.onCreate(); settings = new Settings(this); missingMessageUtil = - new MissedMessageUtil(ClientFactory.clientToken(settings.url(), settings.token())); + new MissedMessageUtil( + ClientFactory.clientToken( + settings.url(), settings.sslSettings(), settings.token())); Log.i("Create " + getClass().getSimpleName()); } @@ -81,7 +83,7 @@ public class WebSocketService extends Service { } connection = - new WebSocketConnection(settings.url(), settings.token()) + new WebSocketConnection(settings.url(), settings.sslSettings(), settings.token()) .onOpen(this::onOpen) .onClose(() -> foreground(getString(R.string.websocket_closed))) .onBadRequest(this::onBadRequest) diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..4522aca --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 3625112..a090611 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -25,7 +25,7 @@ android:layout_weight="1" android:minWidth="40dp" android:minHeight="40dp" - android:visibility="invisible" + android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.501" app:layout_constraintStart_toStartOf="parent" @@ -38,9 +38,10 @@ android:layout_weight="1" android:minWidth="40dp" android:minHeight="40dp" - android:visibility="invisible" - app:layout_constraintEnd_toEndOf="@+id/checkurl" - app:layout_constraintStart_toStartOf="@+id/login" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintHorizontal_bias="0.501" app:layout_constraintTop_toTopOf="@+id/login" /> + app:layout_constraintEnd_toStartOf="@+id/advanced_settings" + app:layout_constraintStart_toStartOf="@+id/gotify_url" + app:layout_constraintTop_toBottomOf="@+id/gotify_url" + app:layout_constraintWidth_max="280dp" /> + diff --git a/app/src/main/res/layout/advanced_settings_dialog.xml b/app/src/main/res/layout/advanced_settings_dialog.xml new file mode 100644 index 0000000..bf569ff --- /dev/null +++ b/app/src/main/res/layout/advanced_settings_dialog.xml @@ -0,0 +1,24 @@ + + + + + +