forked from gotify/android
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLoginActivity.kt
More file actions
293 lines (253 loc) · 11 KB
/
Copy pathLoginActivity.kt
File metadata and controls
293 lines (253 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
package com.github.gotify.login
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
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.ApiException
import com.github.gotify.api.Callback
import com.github.gotify.api.Callback.SuccessCallback
import com.github.gotify.api.CertUtils
import com.github.gotify.api.ClientFactory
import com.github.gotify.client.ApiClient
import com.github.gotify.client.api.ClientApi
import com.github.gotify.client.api.UserApi
import com.github.gotify.client.model.Client
import com.github.gotify.client.model.VersionInfo
import com.github.gotify.databinding.ActivityLoginBinding
import com.github.gotify.databinding.ClientNameDialogBinding
import com.github.gotify.init.InitializationActivity
import com.github.gotify.log.LogsActivity
import com.github.gotify.log.UncaughtExceptionHandler
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import java.security.cert.X509Certificate
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.tinylog.kotlin.Logger
internal class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private lateinit var settings: Settings
private var disableSslValidation = false
private var caCertContents: String? = null
private lateinit var advancedDialog: AdvancedDialog
private val certificateDialogResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
try {
require(result.resultCode == RESULT_OK) { "result was ${result.resultCode}" }
requireNotNull(result.data) { "file path was null" }
val uri = result.data!!.data ?: throw IllegalArgumentException("file path was null")
val fileStream = contentResolver.openInputStream(uri)
?: throw IllegalArgumentException("file path was invalid")
val content = Utils.readFileFromStream(fileStream)
val name = getNameOfCertContent(content)
// temporarily set the contents (don't store to settings until they decide to login)
caCertContents = content
advancedDialog.showRemoveCACertificate(name)
} catch (e: Exception) {
Utils.showSnackBar(this, getString(R.string.select_ca_failed, e.message))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
UncaughtExceptionHandler.registerCurrentThread()
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
Logger.info("Entering ${javaClass.simpleName}")
settings = Settings(this)
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
binding.gotifyUrlEditext.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
invalidateUrl()
}
override fun afterTextChanged(editable: Editable) {}
})
binding.checkurl.setOnClickListener { doCheckUrl() }
binding.openLogs.setOnClickListener { openLogs() }
binding.advancedSettings.setOnClickListener { toggleShowAdvanced() }
binding.login.setOnClickListener { doLogin() }
}
private fun invalidateUrl() {
binding.username.visibility = View.GONE
binding.password.visibility = View.GONE
binding.login.visibility = View.GONE
binding.checkurl.text = getString(R.string.check_url)
}
private fun doCheckUrl() {
val url = binding.gotifyUrlEditext.text.toString().trim().trimEnd('/')
val parsedUrl = url.toHttpUrlOrNull()
if (parsedUrl == null) {
Utils.showSnackBar(this, "Invalid URL (include http:// or https://)")
return
}
if ("http" == parsedUrl.scheme) {
showHttpWarning()
}
binding.checkurlProgress.visibility = View.VISIBLE
binding.checkurl.visibility = View.GONE
try {
ClientFactory.versionApi(url, tempSslSettings())
.version
.enqueue(Callback.callInUI(this, onValidUrl(url), onInvalidUrl(url)))
} catch (e: Exception) {
binding.checkurlProgress.visibility = View.GONE
binding.checkurl.visibility = View.VISIBLE
val errorMsg = getString(R.string.version_failed, "$url/version", e.message)
Utils.showSnackBar(this, errorMsg)
}
}
private fun showHttpWarning() {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.warning)
.setCancelable(true)
.setMessage(R.string.http_warning)
.setPositiveButton(R.string.i_understand, null)
.show()
}
private fun openLogs() {
startActivity(Intent(this, LogsActivity::class.java))
}
private fun toggleShowAdvanced() {
val selectedCertName = if (caCertContents != null) {
getNameOfCertContent(caCertContents!!)
} else {
null
}
advancedDialog = AdvancedDialog(this, layoutInflater)
.onDisableSSLChanged { _, disable ->
invalidateUrl()
disableSslValidation = disable
}
.onClickSelectCaCertificate {
invalidateUrl()
doSelectCACertificate()
}
.onClickRemoveCaCertificate {
invalidateUrl()
caCertContents = null
}
.show(disableSslValidation, selectedCertName)
}
private fun doSelectCACertificate() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
// we don't really care what kind of file it is as long as we can parse it
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
try {
certificateDialogResultLauncher.launch(
Intent.createChooser(intent, getString(R.string.select_ca_file))
)
} catch (e: ActivityNotFoundException) {
// case for user not having a file browser installed
Utils.showSnackBar(this, getString(R.string.please_install_file_browser))
}
}
private fun getNameOfCertContent(content: String): String {
val ca = CertUtils.parseCertificate(content)
return (ca as X509Certificate).subjectDN.name
}
private fun onValidUrl(url: String): SuccessCallback<VersionInfo> {
return Callback.SuccessBody { version ->
settings.url = url
binding.checkurlProgress.visibility = View.GONE
binding.checkurl.visibility = View.VISIBLE
binding.checkurl.text = getString(R.string.found_gotify_version, version.version)
binding.username.visibility = View.VISIBLE
binding.username.requestFocus()
binding.password.visibility = View.VISIBLE
binding.login.visibility = View.VISIBLE
}
}
private fun onInvalidUrl(url: String): Callback.ErrorCallback {
return Callback.ErrorCallback { exception ->
binding.checkurlProgress.visibility = View.GONE
binding.checkurl.visibility = View.VISIBLE
Utils.showSnackBar(this, versionError(url, exception))
}
}
private fun doLogin() {
val username = binding.usernameEditext.text.toString()
val password = binding.passwordEditext.text.toString()
binding.login.visibility = View.GONE
binding.loginProgress.visibility = View.VISIBLE
val client = ClientFactory.basicAuth(settings.url, tempSslSettings(), username, password)
client.createService(UserApi::class.java)
.currentUser()
.enqueue(
Callback.callInUI(
this,
onSuccess = { newClientDialog(client) },
onError = { onInvalidLogin() }
)
)
}
private fun onInvalidLogin() {
binding.login.visibility = View.VISIBLE
binding.loginProgress.visibility = View.GONE
Utils.showSnackBar(this, getString(R.string.wronguserpw))
}
private fun newClientDialog(client: ApiClient) {
val clientDialogBinding = ClientNameDialogBinding.inflate(layoutInflater)
val clientDialogEditext = clientDialogBinding.clientNameEditext
clientDialogEditext.setText(Build.MODEL)
MaterialAlertDialogBuilder(this)
.setTitle(R.string.create_client_title)
.setMessage(R.string.create_client_message)
.setView(clientDialogBinding.root)
.setPositiveButton(R.string.create, doCreateClient(client, clientDialogEditext))
.setNegativeButton(R.string.cancel) { _, _ -> onCancelClientDialog() }
.setCancelable(false)
.show()
}
private fun doCreateClient(
client: ApiClient,
nameProvider: TextInputEditText
): DialogInterface.OnClickListener {
return DialogInterface.OnClickListener { _, _ ->
val newClient = Client().name(nameProvider.text.toString())
client.createService(ClientApi::class.java)
.createClient(newClient)
.enqueue(
Callback.callInUI(
this,
onSuccess = Callback.SuccessBody { client -> onCreatedClient(client) },
onError = { onFailedToCreateClient() }
)
)
}
}
private fun onCreatedClient(client: Client) {
settings.token = client.token
settings.validateSSL = !disableSslValidation
settings.cert = caCertContents
Utils.showSnackBar(this, getString(R.string.created_client))
startActivity(Intent(this, InitializationActivity::class.java))
finish()
}
private fun onFailedToCreateClient() {
Utils.showSnackBar(this, getString(R.string.create_client_failed))
binding.loginProgress.visibility = View.GONE
binding.login.visibility = View.VISIBLE
}
private fun onCancelClientDialog() {
binding.loginProgress.visibility = View.GONE
binding.login.visibility = View.VISIBLE
}
private fun versionError(url: String, exception: ApiException): String {
return getString(R.string.version_failed_status_code, "$url/version", exception.code)
}
private fun tempSslSettings(): SSLSettings {
return SSLSettings(!disableSslValidation, caCertContents)
}
}