summaryrefslogtreecommitdiffhomepage
path: root/android/src/main/kotlin
diff options
context:
space:
mode:
authorJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2019-06-24 11:23:48 -0300
committerJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2019-06-24 11:23:48 -0300
commit70779ba7aa7aa41a24558f99698d8b106a4ec457 (patch)
tree9e7da4c3685bcd8568154aa25a19abdb21e7f249 /android/src/main/kotlin
parentfa066de0989e814532112bc1f521582dc5940063 (diff)
parent67be4c180a615f2e89f394ec0f2ad2ee10d92a6a (diff)
downloadmullvadvpn-70779ba7aa7aa41a24558f99698d8b106a4ec457.tar.xz
mullvadvpn-70779ba7aa7aa41a24558f99698d8b106a4ec457.zip
Merge branch 'problem-report-on-android'
Diffstat (limited to 'android/src/main/kotlin')
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt2
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ProblemReportFragment.kt206
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt11
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt78
4 files changed, 294 insertions, 3 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt
index 4b5414da71..1731ce2a56 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt
@@ -18,6 +18,7 @@ import android.support.v4.app.FragmentActivity
import net.mullvad.mullvadvpn.dataproxy.AccountCache
import net.mullvad.mullvadvpn.dataproxy.LocationInfoCache
+import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport
import net.mullvad.mullvadvpn.dataproxy.RelayListListener
import net.mullvad.mullvadvpn.model.RelaySettings
import net.mullvad.mullvadvpn.model.Settings
@@ -36,6 +37,7 @@ class MainActivity : FragmentActivity() {
val accountCache = AccountCache(this)
val locationInfoCache = LocationInfoCache(asyncDaemon)
+ val problemReport = MullvadProblemReport()
var relayListListener = RelayListListener(this)
private var waitForDaemonJob: Job? = null
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ProblemReportFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ProblemReportFragment.kt
new file mode 100644
index 0000000000..1e72aef5f6
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ProblemReportFragment.kt
@@ -0,0 +1,206 @@
+package net.mullvad.mullvadvpn
+
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+
+import android.content.Context
+import android.os.Bundle
+import android.support.v4.app.Fragment
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.EditText
+import android.widget.TextView
+import android.widget.ViewSwitcher
+
+import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport
+
+class ProblemReportFragment : Fragment() {
+ private lateinit var problemReport: MullvadProblemReport
+
+ private lateinit var bodyContainer: ViewSwitcher
+ private lateinit var userEmailInput: EditText
+ private lateinit var userMessageInput: EditText
+ private lateinit var sendButton: Button
+
+ private lateinit var sendingSpinner: View
+ private lateinit var sentSuccessfullyIcon: View
+ private lateinit var failedToSendIcon: View
+
+ private lateinit var sendStatusLabel: TextView
+ private lateinit var sendDetailsLabel: TextView
+ private lateinit var responseMessageLabel: TextView
+ private lateinit var responseEmailLabel: TextView
+
+ private lateinit var editMessageButton: Button
+ private lateinit var tryAgainButton: Button
+
+ private var sendReportJob: Job? = null
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+
+ val parentActivity = context as MainActivity
+
+ problemReport = parentActivity.problemReport
+ problemReport.collect()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val view = inflater.inflate(R.layout.problem_report, container, false)
+
+ view.findViewById<View>(R.id.back).setOnClickListener {
+ activity?.onBackPressed()
+ }
+
+ bodyContainer = view.findViewById<ViewSwitcher>(R.id.body_container)
+ userEmailInput = view.findViewById<EditText>(R.id.user_email)
+ userMessageInput = view.findViewById<EditText>(R.id.user_message)
+ sendButton = view.findViewById<Button>(R.id.send_button)
+
+ sendingSpinner = view.findViewById<View>(R.id.sending_spinner)
+ sentSuccessfullyIcon = view.findViewById<View>(R.id.sent_successfully_icon)
+ failedToSendIcon = view.findViewById<View>(R.id.failed_to_send_icon)
+
+ sendStatusLabel = view.findViewById<TextView>(R.id.send_status)
+ sendDetailsLabel = view.findViewById<TextView>(R.id.send_details)
+ responseMessageLabel = view.findViewById<TextView>(R.id.response_message)
+ responseEmailLabel = view.findViewById<TextView>(R.id.response_email)
+
+ editMessageButton = view.findViewById<Button>(R.id.edit_message_button)
+ tryAgainButton = view.findViewById<Button>(R.id.try_again_button)
+
+ sendButton.setOnClickListener {
+ sendReportJob?.cancel()
+ sendReportJob = sendReport()
+ }
+
+ editMessageButton.setOnClickListener {
+ showForm()
+ }
+
+ tryAgainButton.setOnClickListener {
+ sendReportJob = sendReport()
+ }
+
+ userEmailInput.setText(problemReport.userEmail)
+ userMessageInput.setText(problemReport.userMessage)
+
+ setSendButtonEnabled(!userMessageInput.text.isEmpty())
+ userMessageInput.addTextChangedListener(InputWatcher())
+
+ return view
+ }
+
+ override fun onDestroyView() {
+ sendReportJob?.cancel()
+
+ problemReport.userEmail = userEmailInput.text.toString()
+ problemReport.userMessage = userMessageInput.text.toString()
+ problemReport.deleteReportFile()
+
+ super.onDestroyView()
+ }
+
+ private fun sendReport() = GlobalScope.launch(Dispatchers.Main) {
+ val userEmail = userEmailInput.text.toString()
+
+ problemReport.userEmail = userEmail
+ problemReport.userMessage = userMessageInput.text.toString()
+
+ showSendingScreen()
+
+ if (problemReport.send().await()) {
+ clearForm()
+ showSuccessScreen(userEmail)
+ } else {
+ showErrorScreen()
+ }
+ }
+
+ private fun clearForm() {
+ userEmailInput.setText("")
+ userMessageInput.setText("")
+
+ problemReport.userEmail = ""
+ problemReport.userMessage = ""
+ }
+
+ private fun showForm() {
+ bodyContainer.displayedChild = 0
+ }
+
+ private fun showSendingScreen() {
+ bodyContainer.displayedChild = 1
+
+ sendingSpinner.visibility = View.VISIBLE
+ sentSuccessfullyIcon.visibility = View.GONE
+ failedToSendIcon.visibility = View.GONE
+
+ sendStatusLabel.visibility = View.VISIBLE
+ sendDetailsLabel.visibility = View.GONE
+ responseMessageLabel.visibility = View.GONE
+ responseEmailLabel.visibility = View.GONE
+
+ sendStatusLabel.setText(R.string.sending)
+
+ editMessageButton.visibility = View.GONE
+ tryAgainButton.visibility = View.GONE
+ }
+
+ private fun showSuccessScreen(userEmail: String) {
+ sendingSpinner.visibility = View.GONE
+
+ sentSuccessfullyIcon.visibility = View.VISIBLE
+ sendStatusLabel.visibility = View.VISIBLE
+ sendDetailsLabel.visibility = View.VISIBLE
+
+ if (!userEmail.isEmpty()) {
+ responseMessageLabel.visibility = View.VISIBLE
+ responseEmailLabel.visibility = View.VISIBLE
+ responseEmailLabel.text = userEmail
+ }
+
+ sendStatusLabel.setText(R.string.sent)
+ sendDetailsLabel.setText(R.string.sent_thanks)
+ }
+
+ private fun showErrorScreen() {
+ sendingSpinner.visibility = View.GONE
+
+ failedToSendIcon.visibility = View.VISIBLE
+ sendStatusLabel.visibility = View.VISIBLE
+ sendDetailsLabel.visibility = View.VISIBLE
+
+ sendStatusLabel.setText(R.string.failed_to_send)
+ sendDetailsLabel.setText(R.string.failed_to_send_details)
+
+ editMessageButton.visibility = View.VISIBLE
+ tryAgainButton.visibility = View.VISIBLE
+ }
+
+ private fun setSendButtonEnabled(enabled: Boolean) {
+ sendButton.setEnabled(enabled)
+ sendButton.alpha = if (enabled) 1.0F else 0.5F
+ }
+
+ inner class InputWatcher : TextWatcher {
+ override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {}
+
+ override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {}
+
+ override fun afterTextChanged(text: Editable) {
+ setSendButtonEnabled(!text.isEmpty())
+ }
+ }
+}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt
index ee89ed7945..3400d43e58 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt
@@ -34,7 +34,12 @@ class SettingsFragment : Fragment() {
activity?.finishAndRemoveTask()
}
- view.findViewById<View>(R.id.account).setOnClickListener { openAccountSettings() }
+ view.findViewById<View>(R.id.account).setOnClickListener {
+ openSubFragment(AccountFragment())
+ }
+ view.findViewById<View>(R.id.report_a_problem).setOnClickListener {
+ openSubFragment(ProblemReportFragment())
+ }
remainingTimeLabel = RemainingTimeLabel(parentActivity, view)
@@ -51,7 +56,7 @@ class SettingsFragment : Fragment() {
super.onDestroyView()
}
- private fun openAccountSettings() {
+ private fun openSubFragment(fragment: Fragment) {
fragmentManager?.beginTransaction()?.apply {
setCustomAnimations(
R.anim.fragment_enter_from_right,
@@ -59,7 +64,7 @@ class SettingsFragment : Fragment() {
R.anim.fragment_half_enter_from_left,
R.anim.fragment_exit_to_right
)
- replace(R.id.main_fragment, AccountFragment())
+ replace(R.id.main_fragment, fragment)
addToBackStack(null)
commit()
}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt
new file mode 100644
index 0000000000..d1794fbc3f
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt
@@ -0,0 +1,78 @@
+package net.mullvad.mullvadvpn.dataproxy
+
+import java.io.File
+
+import kotlinx.coroutines.async
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+
+const val PROBLEM_REPORT_PATH = "/data/data/net.mullvad.mullvadvpn/problem_report.txt"
+
+class MullvadProblemReport {
+ private var collectJob: Deferred<Boolean>? = null
+ private var sendJob: Deferred<Boolean>? = null
+
+ var userEmail = ""
+ var userMessage = ""
+
+ val isActive: Boolean
+ get() {
+ synchronized(this) {
+ val collectJob = this.collectJob
+ val sendJob = this.sendJob
+
+ return (collectJob != null && collectJob.isActive) ||
+ (sendJob != null && sendJob.isActive)
+ }
+ }
+
+ init {
+ System.loadLibrary("mullvad_jni")
+ }
+
+ fun collect() {
+ synchronized(this) {
+ if (!isActive) {
+ collectJob = GlobalScope.async(Dispatchers.Default) {
+ deleteReportFile()
+ collectReport(PROBLEM_REPORT_PATH)
+ }
+ }
+ }
+ }
+
+ fun send(): Deferred<Boolean> {
+ synchronized(this) {
+ var currentJob = sendJob
+
+ if (currentJob == null || currentJob.isCompleted) {
+ currentJob = GlobalScope.async(Dispatchers.Default) {
+ val result = (collectJob?.await() ?: false) &&
+ sendProblemReport(userEmail, userMessage, PROBLEM_REPORT_PATH)
+
+ if (result) {
+ deleteReportFile()
+ }
+
+ result
+ }
+
+ sendJob = currentJob
+ }
+
+ return currentJob
+ }
+ }
+
+ fun deleteReportFile() {
+ File(PROBLEM_REPORT_PATH).delete()
+ }
+
+ private external fun collectReport(reportPath: String): Boolean
+ private external fun sendProblemReport(
+ userEmail: String,
+ userMessage: String,
+ reportPath: String
+ ): Boolean
+}