diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2019-06-24 11:23:48 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2019-06-24 11:23:48 -0300 |
| commit | 70779ba7aa7aa41a24558f99698d8b106a4ec457 (patch) | |
| tree | 9e7da4c3685bcd8568154aa25a19abdb21e7f249 /android/src | |
| parent | fa066de0989e814532112bc1f521582dc5940063 (diff) | |
| parent | 67be4c180a615f2e89f394ec0f2ad2ee10d92a6a (diff) | |
| download | mullvadvpn-70779ba7aa7aa41a24558f99698d8b106a4ec457.tar.xz mullvadvpn-70779ba7aa7aa41a24558f99698d8b106a4ec457.zip | |
Merge branch 'problem-report-on-android'
Diffstat (limited to 'android/src')
11 files changed, 577 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 +} diff --git a/android/src/main/res/drawable/blue_button_background.xml b/android/src/main/res/drawable/blue_button_background.xml new file mode 100644 index 0000000000..e53b0be561 --- /dev/null +++ b/android/src/main/res/drawable/blue_button_background.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="false"> + <shape android:shape="rectangle"> + <corners android:radius="4dp"/> + <solid android:color="@color/blue80"/> + </shape> + </item> + + <item android:state_pressed="true"> + <shape android:shape="rectangle"> + <corners android:radius="4dp"/> + <solid android:color="@color/blue60"/> + </shape> + </item> +</selector> diff --git a/android/src/main/res/drawable/input_text_background.xml b/android/src/main/res/drawable/input_text_background.xml new file mode 100644 index 0000000000..545bc362f8 --- /dev/null +++ b/android/src/main/res/drawable/input_text_background.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle" + > + <corners android:radius="4dp"/> + <solid android:color="@color/white"/> +</shape> diff --git a/android/src/main/res/layout/problem_report.xml b/android/src/main/res/layout/problem_report.xml new file mode 100644 index 0000000000..c3d782e7ad --- /dev/null +++ b/android/src/main/res/layout/problem_report.xml @@ -0,0 +1,195 @@ +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/darkBlue" + android:orientation="vertical" + android:gravity="left" + android:elevation="2dp" + > + <LinearLayout android:id="@+id/back" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="12dp" + android:orientation="horizontal" + android:gravity="center_vertical | left" + android:clickable="true" + android:background="?android:attr/selectableItemBackground" + > + <ImageView + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginRight="8dp" + android:src="@drawable/icon_back" + /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/white60" + android:textSize="13sp" + android:textStyle="bold" + android:text="@string/settings" + /> + </LinearLayout> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0" + android:layout_marginTop="4dp" + android:layout_marginBottom="8dp" + android:layout_marginHorizontal="24dp" + android:textColor="@color/white" + android:textSize="32sp" + android:textStyle="bold" + android:text="@string/report_a_problem" + /> + <ViewSwitcher android:id="@+id/body_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + > + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + > + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0" + android:layout_marginBottom="24dp" + android:layout_marginHorizontal="24dp" + android:textColor="@color/white80" + android:textSize="13sp" + android:text="@string/problem_report_description" + /> + <EditText android:id="@+id/user_email" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="0" + android:layout_marginBottom="12dp" + android:layout_marginHorizontal="22dp" + android:singleLine="true" + android:hint="@string/user_email_hint" + style="@style/InputText" + /> + <EditText android:id="@+id/user_message" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:layout_marginHorizontal="22dp" + android:singleLine="false" + android:hint="@string/user_message_hint" + android:gravity="top" + style="@style/InputText" + /> + <Button android:id="@+id/send_button" + android:layout_marginHorizontal="24dp" + android:layout_marginVertical="16dp" + android:enabled="false" + android:text="@string/send" + style="@style/GreenButton" + /> + </LinearLayout> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginVertical="16dp" + android:layout_marginHorizontal="24dp" + android:orientation="vertical" + > + <FrameLayout + android:layout_width="60dp" + android:layout_height="60dp" + android:layout_gravity="center_horizontal" + android:layout_marginBottom="32dp" + > + <ProgressBar android:id="@+id/sending_spinner" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:indeterminate="true" + android:indeterminateOnly="true" + android:indeterminateDuration="600" + android:indeterminateDrawable="@drawable/icon_spinner" + /> + <ImageView android:id="@+id/sent_successfully_icon" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:src="@drawable/icon_success" + android:visibility="gone" + /> + <ImageView android:id="@+id/failed_to_send_icon" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:src="@drawable/icon_fail" + android:visibility="gone" + /> + </FrameLayout> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:textColor="@color/green" + android:textSize="16sp" + android:textStyle="bold" + android:text="@string/secure_connection" + android:textAllCaps="true" + /> + <TextView android:id="@+id/send_status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:textColor="@color/white" + android:textSize="34sp" + android:textStyle="bold" + android:text="@string/sending" + /> + <TextView android:id="@+id/send_details" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/white60" + android:textSize="13sp" + android:text="@string/sent_thanks" + android:visibility="gone" + /> + <TextView android:id="@+id/response_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/white60" + android:textSize="13sp" + android:text="@string/sent_contact" + android:visibility="gone" + /> + <TextView android:id="@+id/response_email" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/white" + android:textSize="13sp" + android:textStyle="bold" + android:visibility="gone" + /> + + <Space + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + /> + + <Button android:id="@+id/edit_message_button" + android:layout_marginTop="16dp" + android:text="@string/edit_message" + android:visibility="gone" + style="@style/BlueButton" + /> + <Button android:id="@+id/try_again_button" + android:layout_marginTop="16dp" + android:text="@string/try_again" + android:visibility="gone" + style="@style/GreenButton" + /> + </LinearLayout> + </ViewSwitcher> +</LinearLayout> diff --git a/android/src/main/res/layout/settings.xml b/android/src/main/res/layout/settings.xml index 664a2dc790..84c94dcf95 100644 --- a/android/src/main/res/layout/settings.xml +++ b/android/src/main/res/layout/settings.xml @@ -66,6 +66,34 @@ android:src="@drawable/icon_chevron" /> </LinearLayout> + <LinearLayout android:id="@+id/report_a_problem" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:paddingHorizontal="16dp" + android:background="@drawable/cell_button_background" + android:clickable="true" + android:gravity="center" + > + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingHorizontal="8dp" + android:paddingVertical="17dp" + android:textColor="@color/white" + android:textSize="20sp" + android:textStyle="bold" + android:text="@string/report_a_problem" + /> + <ImageView + android:layout_width="14dp" + android:layout_height="24dp" + android:layout_weight="0" + android:alpha="0.6" + android:src="@drawable/icon_chevron" + /> + </LinearLayout> <Button android:id="@+id/quit_button" android:layout_marginTop="24dp" android:layout_marginLeft="24dp" diff --git a/android/src/main/res/values/colors.xml b/android/src/main/res/values/colors.xml index 0e7b0cea4e..e882682904 100644 --- a/android/src/main/res/values/colors.xml +++ b/android/src/main/res/values/colors.xml @@ -2,10 +2,12 @@ <color name="colorPrimary">#294D73</color> <color name="blue">#294D73</color> <color name="blue80">#CC294D73</color> + <color name="blue60">#99294D73</color> <color name="blue40">#66294D73</color> <color name="blue20">#33294D73</color> <color name="darkBlue">#192E45</color> <color name="white">#FFFFFF</color> + <color name="white80">#CCFFFFFF</color> <color name="white60">#99FFFFFF</color> <color name="white40">#66FFFFFF</color> <color name="white20">#33FFFFFF</color> diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index eef3679d2a..b0087e1942 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -16,6 +16,7 @@ <string name="settings_account">Account</string> <string name="less_than_a_day_left">less than a day left</string> <string name="out_of_time">Out of time</string> + <string name="report_a_problem">Report a problem</string> <string name="quit">Quit</string> <string name="account_number">Account number</string> @@ -24,6 +25,26 @@ <string name="paid_until">Paid until</string> <string name="log_out">Log out</string> + <string name="problem_report_description"> + To help you more effectively, your app\'s log file will be attached to this message. Your + data will remain secure and private, as it is anonymised before being sent over an encrypted + channel. + </string> + <string name="user_email_hint">Your email (optional)</string> + <string name="user_message_hint">Describe your problem</string> + <string name="send">Send</string> + <string name="sending">Sending...</string> + <string name="sent">Sent</string> + <string name="failed_to_send">Failed to send</string> + <string name="sent_thanks">Thanks! We will look into this.</string> + <string name="sent_contact">If needed we will contact you on</string> + <string name="failed_to_send_details"> + You may need to go back to the app\'s main screen and click Disconnect before trying again. + Don\'t worry, the information you entered will remain in the form. + </string> + <string name="edit_message">Edit message</string> + <string name="try_again">Try again</string> + <string name="unsecured_connection">Unsecured connection</string> <string name="creating_secure_connection">Creating secure connection</string> <string name="secure_connection">Secure connection</string> diff --git a/android/src/main/res/values/styles.xml b/android/src/main/res/values/styles.xml index 43c29dc1bb..2ace57e88b 100644 --- a/android/src/main/res/values/styles.xml +++ b/android/src/main/res/values/styles.xml @@ -4,6 +4,15 @@ <item name="android:windowBackground">@color/blue</item> </style> + <style name="InputText" parent="Widget.AppCompat.EditText"> + <item name="android:padding">14dp</item> + <item name="android:background">@drawable/input_text_background</item> + <item name="android:textCursorDrawable">@drawable/text_input_cursor</item> + <item name="android:textColorHint">@color/blue40</item> + <item name="android:textColor">@color/blue</item> + <item name="android:textSize">13sp</item> + </style> + <style name="Button" parent="Widget.AppCompat.Button.Borderless"> <item name="android:layout_height">44dp</item> <item name="android:layout_width">match_parent</item> @@ -21,6 +30,10 @@ <item name="android:background">@drawable/red_button_background</item> </style> + <style name="BlueButton" parent="Button"> + <item name="android:background">@drawable/blue_button_background</item> + </style> + <style name="White20Button" parent="Button"> <item name="android:background">@drawable/white20_button_background</item> </style> |
