diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessRouting.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessRouting.kt index b8d6401b217500d4e8b8882ef721997a2dc39184..d989c3e9b0abf7d6dcc6a47efbb875ad0b4f51c8 100644 --- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessRouting.kt +++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessRouting.kt @@ -107,6 +107,7 @@ import org.videolan.vlc.BuildConfig import org.videolan.vlc.gui.dialogs.getPlaylistByName import org.videolan.vlc.gui.helpers.AudioUtil import org.videolan.vlc.gui.helpers.BitmapUtil +import org.videolan.vlc.gui.helpers.FeedbackUtil import org.videolan.vlc.gui.helpers.VectorDrawableUtil import org.videolan.vlc.gui.helpers.getBitmapFromDrawable import org.videolan.vlc.gui.helpers.getColoredBitmapFromColor @@ -266,6 +267,67 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) { call.respondJson(convertToJson(logs)) } + // Prepare the feedback data + post("/feedback-form") { + verifyLogin(settings) + + val formParameters = try { + call.receiveParameters() + } catch (e: Exception) { + Log.w(this::class.java.simpleName, "Failed to parse form parameters. ${e.message}", e) + call.respond(HttpStatusCode.BadRequest) + return@post + } + + val feedbackType = formParameters.get("type")?.toInt() ?: 0 + val includeML = formParameters.get("includeML") == "true" + val includeLogs = formParameters.get("includeLogs") == "true" + val subject = formParameters.get("subject") ?: "" + val message = formParameters.get("message") ?: "" + + if ((includeLogs || includeML) && !settings.getBoolean(REMOTE_ACCESS_LOGS, false)) { + call.respond(HttpStatusCode.Forbidden) + return@post + } + + var zipFile: String? = null + + val externalPath = RemoteAccessServer.getInstance(appContext).downloadFolder + val logcatZipPath = "$externalPath/logcat.zip" + + // generate logs + if (includeLogs) { + File(externalPath).mkdirs() + RemoteAccessServer.getInstance(appContext).gatherLogs(logcatZipPath) + } + val dbPath = "$externalPath${Medialibrary.VLC_MEDIA_DB_NAME}" + + //generate ML + if (includeML) { + File(externalPath).mkdirs() + val db = File(appContext.getDir("db", Context.MODE_PRIVATE).toString() + Medialibrary.VLC_MEDIA_DB_NAME) + val dbFile = File(dbPath) + FileUtils.copyFile(db, dbFile) + } + + //Zip needed files + if (File(logcatZipPath).exists() || File(dbPath).exists()) { + zipFile = "feedback_report.zip" + val dbZipPath = "$externalPath/$zipFile" + val filesToZip = mutableListOf<String>() + if (File(logcatZipPath).exists()) filesToZip.add(logcatZipPath) + if (File(dbPath).exists()) filesToZip.add(dbPath) + FileUtils.zip(filesToZip.toTypedArray(), dbZipPath) + filesToZip.forEach { FileUtils.deleteFile(it) } + } + + val completeMessage = "$message\r\n\r\n${FeedbackUtil.generateUsefulInfo(appContext)}" + + val mail = if (feedbackType == 3 && BuildConfig.BETA) FeedbackUtil.SupportType.CRASH_REPORT_EMAIL.email else FeedbackUtil.SupportType.SUPPORT_EMAIL.email + val result = RemoteAccessServer.FeedbackResult(mail, FeedbackUtil.generateSubject(subject, feedbackType), completeMessage, zipFile) + + call.respondJson(convertToJson(result)) + } // Get the translation string list get("/translation") { call.respondJson(convertToJson(TranslationMapping.generateTranslations(appContext.getContextWithLocale(AppContextProvider.locale)))) diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessServer.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessServer.kt index 609087140d125ed6900d4899e966023ce0467fe6..4de80e452ff7516e46909ebfc08af36dac31cede 100644 --- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessServer.kt +++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessServer.kt @@ -97,10 +97,12 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import org.slf4j.LoggerFactory import org.videolan.libvlc.MediaPlayer import org.videolan.libvlc.interfaces.IMedia +import org.videolan.libvlc.util.AndroidUtil import org.videolan.libvlc.util.MediaBrowser import org.videolan.medialibrary.MLServiceLocator import org.videolan.medialibrary.interfaces.media.MediaWrapper import org.videolan.medialibrary.media.MediaLibraryItem +import org.videolan.resources.AppContextProvider import org.videolan.resources.VLCInstance import org.videolan.tools.AppScope import org.videolan.tools.KEYSTORE_PASSWORD @@ -109,6 +111,7 @@ import org.videolan.tools.REMOTE_ACCESS_NETWORK_BROWSER_CONTENT import org.videolan.tools.Settings import org.videolan.tools.SingletonHolder import org.videolan.tools.putSingle +import org.videolan.vlc.DebugLogService import org.videolan.vlc.PlaybackService import org.videolan.vlc.PlaybackService.Companion.playerSleepTime import org.videolan.vlc.gui.DialogActivity @@ -117,12 +120,14 @@ import org.videolan.vlc.remoteaccessserver.ssl.SecretGenerator import org.videolan.vlc.remoteaccessserver.websockets.RemoteAccessWebSockets import org.videolan.vlc.remoteaccessserver.websockets.RemoteAccessWebSockets.setupWebSockets import org.videolan.vlc.util.FileUtils +import org.videolan.vlc.util.Permissions import org.videolan.vlc.util.isSchemeSMB import org.videolan.vlc.viewmodels.CallBackDelegate import org.videolan.vlc.viewmodels.ICallBackHandler import org.videolan.vlc.viewmodels.browser.IPathOperationDelegate import org.videolan.vlc.viewmodels.browser.PathOperationDelegate import java.io.File +import java.io.IOException import java.math.BigInteger import java.net.InetAddress import java.net.NetworkInterface @@ -164,6 +169,9 @@ class RemoteAccessServer(private val context: Context) : PlaybackService.Callbac private val otgDevice = context.getString(org.videolan.vlc.R.string.otg_device_title) + var client: DebugLogService.Client? = null + + private val miniPlayerObserver = androidx.lifecycle.Observer<Boolean> { playing -> AppScope.launch { val isPlaying = service?.isPlaying == true || playing @@ -324,6 +332,7 @@ class RemoteAccessServer(private val context: Context) : PlaybackService.Callbac withContext(Dispatchers.IO) { RemoteAccessWebSockets.closeAllSessions() if (::engine.isInitialized) engine.stop() + client?.release() } } @@ -901,6 +910,90 @@ class RemoteAccessServer(private val context: Context) : PlaybackService.Callbac return list } + suspend fun gatherLogs(logcatZipPath: String) { + var waitForClient = true + var started = false + val gatheringLogsStart = System.currentTimeMillis() + //initiate a log to wait for + val logMessage = "Starting collecting logs at ${System.currentTimeMillis()}" + + client = DebugLogService.Client(context, object : DebugLogService.Client.Callback { + override fun onStarted(logList: List<String>) { + started = true + Log.d("LogsGathering", logMessage) + } + + override fun onStopped() { + } + + override fun onLog(msg: String) { + //Wait for the log to initiate a save to avoid ANR + if (msg.contains(logMessage)) { + if (AndroidUtil.isOOrLater && !Permissions.canWriteStorage()) + waitForClient = false + else + client?.save() + } + } + + override fun onSaved(success: Boolean, path: String) { + if (!success) { + client?.stop() + waitForClient = false + return + } + client?.stop() + val filesToAdd = mutableListOf(path) + //add previous crash logs + try { + AppContextProvider.appContext.getExternalFilesDir(null)?.absolutePath?.let { folder -> + File(folder).listFiles()?.forEach { + if (it.isFile && (it.name.contains("crash_") || it.name.contains("logcat_"))) filesToAdd.add(it.path) + } + } + } catch (exception: IOException) { + Log.w("LogsGathering", exception.message, exception) + client?.stop() + return + } + + if (!FileUtils.zip(filesToAdd.toTypedArray(), logcatZipPath)) { + client?.stop() + return + } + try { + filesToAdd.forEach { FileUtils.deleteFile(it) } + } catch (exception: IOException) { + Log.w("LogsGathering", exception.message, exception) + client?.stop() + return + } + + waitForClient = false + + } + + }) + while (!started) { + delay(100) + if (System.currentTimeMillis() > gatheringLogsStart + 4000) { + Log.w("LogsGathering", "Failed to start log gathering") + started = true + waitForClient = false + } + client?.start() == true + } + while (waitForClient) { + delay(100) + if (System.currentTimeMillis() > gatheringLogsStart + 20000) { + Log.w("LogsGathering", "Cannot complete log gathering in time") + waitForClient = false + } + } + client?.release() + client = null + } + abstract class WSMessage(val type: WSMessageType) data class NowPlaying(val title: String, val artist: String, val playing: Boolean, val isVideoPlaying: Boolean, val progress: Long, val duration: Long, val id: Long, val artworkURL: String, val uri: String, val volume: Int, val speed: Float, @@ -930,6 +1023,7 @@ class RemoteAccessServer(private val context: Context) : PlaybackService.Callbac data class ArtistResult(val albums: List<PlayQueueItem>, val tracks: List<PlayQueueItem>, val name: String) data class AlbumResult(val tracks: List<PlayQueueItem>, val name: String) data class PlaylistResult(val tracks: List<PlayQueueItem>, val name: String) + data class FeedbackResult(val mail: String, val subject: String, val message: String, val file: String?) fun getSecureUrl(call: ApplicationCall) = "https://${call.request.host()}:${engine.environment.connectors.first { it.type.name == "HTTPS" }.port}" diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/TranslationMapping.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/TranslationMapping.kt index a1c0e81763571427238fdd52c98c7970346462f8..ad30db9a8a419928c54a06b2cb6374941b76a789 100644 --- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/TranslationMapping.kt +++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/TranslationMapping.kt @@ -74,6 +74,7 @@ object TranslationMapping { DIRECTORY_EMPTY(R.string.empty_directory), FORBIDDEN(R.string.ra_forbidden), PLAYBACK_CONTROL_FORBIDDEN(R.string.ra_playback_forbidden), + LOGS_CONTROL_FORBIDDEN(R.string.ra_logs_forbidden), SEND(R.string.send), NEW_CODE(R.string.ra_new_code), CODE_REQUEST_EXPLANATION(R.string.ra_code_requested_explanation), @@ -142,6 +143,30 @@ object TranslationMapping { RESUME(R.string.resume), CONFIRM_RESUME(R.string.confirm_resume), APPLY_PLAYQUEUE(R.string.apply_playqueue), - NO(R.string.no) + NO(R.string.no), + FEEDBACK(R.string.send_feedback), + GET_HELP(R.string.get_help), + REPORT_A_BUG(R.string.report_a_bug), + FEEDBACK_FORUM(R.string.feedback_forum), + FEEDBACK_FORUM_URL(R.string.forum_url), + FEEDBACK_DOC(R.string.read_doc), + FEEDBACK_DOC_URL(R.string.doc_url), + FEEDBACK_SUBTITLE(R.string.email_support), + FEEDBACK_TYPE(R.string.feedback_type), + FEEDBACK_HELP(R.string.get_help), + FEEDBACK_FEATURE(R.string.send_feedback_request), + FEEDBACK_BUG(R.string.report_a_bug), + FEEDBACK_CRASH(R.string.report_crash), + FEEDBACK_SUBJECT(R.string.subject), + FEEDBACK_MESSAGE(R.string.body), + FEEDBACK_MEDIALIBRARY(R.string.include_medialib), + FEEDBACK_LOGS(R.string.include_logs), + FEEDBACK_GENERATE(R.string.remote_access_feedback_generate), + FEEDBACK_PLEASE_WAIT(R.string.please_wait), + FEEDBACK_GENERATING_LOGS(R.string.generating_logs), + FEEDBACK_GENERATED_TITLE(R.string.remote_access_feedback_generated), + FEEDBACK_GENERATED_EXPLANATION(R.string.remote_access_feedback_generated_explanation), + FEEDBACK_GENERATED_FILE_EXPLANATION(R.string.remote_access_feedback_file_explanation), + COPIED(R.string.generic_copied_to_clipboard), } } \ No newline at end of file diff --git a/application/resources/src/main/java/org/videolan/resources/Constants.kt b/application/resources/src/main/java/org/videolan/resources/Constants.kt index 35d4e393914d4c74576624bfbb2a9021296d8838..c6b4b15d8995d79e5a1b893f9872264e36e2a99b 100644 --- a/application/resources/src/main/java/org/videolan/resources/Constants.kt +++ b/application/resources/src/main/java/org/videolan/resources/Constants.kt @@ -242,6 +242,7 @@ const val TV_SEARCH_ACTIVITY = "org.videolan.television.ui.SearchActivity" const val MOBILE_SEARCH_ACTIVITY = "org.videolan.vlc.gui.SearchActivity" const val TV_MAIN_ACTIVITY = "org.videolan.television.ui.MainTvActivity" const val TV_CONFIRMATION_ACTIVITY = "org.videolan.television.ui.dialogs.ConfirmationTvActivity" +const val TV_PREFERENCE_ACTIVITY = "org.videolan.television.ui.preferences.PreferencesActivity" const val MOBILE_MAIN_ACTIVITY = "org.videolan.vlc.gui.MainActivity" const val MOVIEPEDIA_ACTIVITY = "org.videolan.moviepedia.ui.MoviepediaActivity" const val TV_AUDIOPLAYER_ACTIVITY = "org.videolan.television.ui.audioplayer.AudioPlayerActivity" diff --git a/application/resources/src/main/res/values/strings.xml b/application/resources/src/main/res/values/strings.xml index bedd1553c0ee59d44862ded7f2345d793769391a..1bac59251e4e9f8425b030e1e4009ef1516f71ed 100644 --- a/application/resources/src/main/res/values/strings.xml +++ b/application/resources/src/main/res/values/strings.xml @@ -1304,6 +1304,7 @@ <string name="ra_prepare_download">Preparing your download</string> <string name="ra_forbidden">Content disabled in the settings</string> <string name="ra_playback_forbidden">Playback control disabled in the settings</string> + <string name="ra_logs_forbidden">Logs sharing disabled in the settings</string> <string name="ra_code_requested_explanation">To protect your server, the access is protected.\nPlease enter the code displayed in your notifications</string> <string name="ra_ssl_explanation_title">Unsecure connection</string> <string name="ra_ssl_explanation">Your connection is not secure.</string> @@ -1395,4 +1396,15 @@ <string name="generating_logs">Generating logs. Please wait.</string> <string name="default_playback_action">Playback action</string> <string name="files">Files</string> + <string name="remote_access_feedback_generate">Generate report</string> + <string name="remote_access_feedback_generated">Your feedback report has been generated</string> + <string name="remote_access_feedback_generated_explanation">Tap the button below to send it using your default email client. If you encounter any issues, you can copy paste the fields below and send the email yourself.</string> + <string name="remote_access_feedback_file_explanation">You just downloaded the needed file. Please remember to attach it to your email.</string> + <string name="generic_copied_to_clipboard">Copied to clipboard</string> + <string name="feedback_email_warning">No email client installed</string> + <string name="feedback_email_warning_explanation">The easiest way to share your feedback is to use the %1$s feature.\nTo do so, enable the remote access in the settings, follow the instructions and choose the %2$s entry in the menu.</string> + <string name="feedback_email_warning_remote_action">Open the settings</string> + <string name="feedback_email_warning_try_anyway">Try anyway</string> + <string name="forum_url" translatable="false">https://forum.videolan.org/viewforum.php?f=35</string> + <string name="doc_url" translatable="false">https://docs.videolan.me/vlc-user/android/</string> </resources> diff --git a/application/television/src/main/java/org/videolan/television/ui/preferences/PreferencesActivity.kt b/application/television/src/main/java/org/videolan/television/ui/preferences/PreferencesActivity.kt index 1298ea53fde117c276e6ff1fa985162ee46575e9..f91dfe6c486eb7c5abbe6563790b85e74a47c3ce 100644 --- a/application/television/src/main/java/org/videolan/television/ui/preferences/PreferencesActivity.kt +++ b/application/television/src/main/java/org/videolan/television/ui/preferences/PreferencesActivity.kt @@ -38,9 +38,11 @@ import org.videolan.tools.Settings import org.videolan.vlc.PlaybackService import org.videolan.vlc.gui.PinCodeActivity import org.videolan.vlc.gui.PinCodeReason +import org.videolan.vlc.gui.preferences.EXTRA_PREF_END_POINT @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) class PreferencesActivity : BaseTvActivity() { + var extraEndPoint: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -50,6 +52,11 @@ class PreferencesActivity : BaseTvActivity() { val intent = PinCodeActivity.getIntent(this, PinCodeReason.CHECK) startActivityForResult(intent, 0) } + if (savedInstanceState == null) { + if (intent.hasExtra(EXTRA_PREF_END_POINT)) { + extraEndPoint = intent.getStringExtra(EXTRA_PREF_END_POINT) + } + } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { diff --git a/application/television/src/main/java/org/videolan/television/ui/preferences/PreferencesFragment.kt b/application/television/src/main/java/org/videolan/television/ui/preferences/PreferencesFragment.kt index adc4e5b42394d3e4b76e94f720eb0a4179715df4..f01f5f2cb2e9d24d677020b5a6c117df5e2b7f8e 100644 --- a/application/television/src/main/java/org/videolan/television/ui/preferences/PreferencesFragment.kt +++ b/application/television/src/main/java/org/videolan/television/ui/preferences/PreferencesFragment.kt @@ -35,9 +35,24 @@ import androidx.preference.CheckBoxPreference import androidx.preference.Preference import androidx.preference.PreferenceScreen import org.videolan.medialibrary.interfaces.Medialibrary -import org.videolan.resources.* -import org.videolan.tools.* +import org.videolan.resources.AndroidDevices +import org.videolan.resources.KEY_AUDIO_LAST_PLAYLIST +import org.videolan.resources.KEY_CURRENT_AUDIO +import org.videolan.resources.KEY_CURRENT_AUDIO_RESUME_ARTIST +import org.videolan.resources.KEY_CURRENT_AUDIO_RESUME_THUMB +import org.videolan.resources.KEY_CURRENT_AUDIO_RESUME_TITLE +import org.videolan.resources.KEY_CURRENT_MEDIA +import org.videolan.resources.KEY_CURRENT_MEDIA_RESUME +import org.videolan.resources.KEY_MEDIA_LAST_PLAYLIST +import org.videolan.resources.KEY_MEDIA_LAST_PLAYLIST_RESUME +import org.videolan.tools.AUDIO_RESUME_PLAYBACK +import org.videolan.tools.KEY_VIDEO_APP_SWITCH +import org.videolan.tools.PLAYBACK_HISTORY +import org.videolan.tools.RESULT_RESTART +import org.videolan.tools.SCREEN_ORIENTATION +import org.videolan.tools.Settings import org.videolan.tools.Settings.isPinCodeSet +import org.videolan.tools.VIDEO_RESUME_PLAYBACK import org.videolan.vlc.R import org.videolan.vlc.gui.PinCodeActivity import org.videolan.vlc.gui.PinCodeReason @@ -60,6 +75,12 @@ class PreferencesFragment : BasePreferenceFragment(), SharedPreferences.OnShared findPreference<Preference>(KEY_VIDEO_APP_SWITCH)?.isVisible = AndroidDevices.hasPiP findPreference<Preference>("remote_access_category")?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1 findPreference<Preference>("permissions_title")?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1 + (activity as? PreferencesActivity)?.extraEndPoint?.let { + if (it == "remote_access_category") findPreference<Preference>("remote_access_category")?.let { + onPreferenceTreeClick(it) + (activity as? PreferencesActivity)?.extraEndPoint = null + } + } } override fun onStart() { diff --git a/application/vlc-android/AndroidManifest.xml b/application/vlc-android/AndroidManifest.xml index 0b8afcb7c5d4801a455b28f119361e4f4da43263..d8a54c8fb1cbcd14433bcb0b6ef021877a380de8 100644 --- a/application/vlc-android/AndroidManifest.xml +++ b/application/vlc-android/AndroidManifest.xml @@ -57,6 +57,13 @@ android:name="android.hardware.bluetooth" android:required="false"/> + <queries> + <intent> + <action android:name="android.intent.action.SENDTO" /> + <data android:scheme="mailto" /> + </intent> + </queries> + <application> <!-- Enable VLC in Samsung multiwindow mode --> diff --git a/application/vlc-android/res/layout/about_feedback_activity.xml b/application/vlc-android/res/layout/about_feedback_activity.xml index 378bb06e8842eda5ef7aa2e6142748d85d9f7289..92821a6528ac4e67c96331fb1c77af4066692087 100644 --- a/application/vlc-android/res/layout/about_feedback_activity.xml +++ b/application/vlc-android/res/layout/about_feedback_activity.xml @@ -270,17 +270,98 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/email_support_summary" /> + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/email_warning" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="8dp" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/contact_separator" + tools:visibility="visible"> + + <ImageView + android:id="@+id/email_warning_image" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:padding="8dp" + app:layout_constraintEnd_toStartOf="@+id/email_warning_title" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_warning_small" + app:tint="?attr/colorPrimary" /> + + <TextView + android:id="@+id/email_warning_title" + style="@style/VLC.TextViewTitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginEnd="16dp" + android:fontFamily="sans-serif-medium" + android:text="@string/feedback_email_warning" + android:textColor="?attr/colorPrimary" + android:textSize="14sp" + app:layout_constraintBottom_toBottomOf="@+id/email_warning_image" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/email_warning_image" + app:layout_constraintTop_toTopOf="@+id/email_warning_image" /> + + <TextView + android:id="@+id/email_warning_explanation" + style="@style/VLC.TextViewTitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="24dp" + android:layout_marginEnd="16dp" + android:text="@string/feedback_email_warning_explanation" + android:textSize="14sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/email_warning_title" /> + + <Button + android:id="@+id/try_anyway" + style="@style/Widget.MaterialComponents.Button.TextButton.Dialog" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:text="@string/feedback_email_warning_try_anyway" + android:textColor="?attr/font_light" + app:layout_constraintEnd_toStartOf="@+id/open_settings" + app:layout_constraintTop_toTopOf="@+id/open_settings" /> + + <Button + android:id="@+id/open_settings" + style="@style/Widget.MaterialComponents.Button.TextButton.Dialog" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:layout_marginEnd="16dp" + android:text="@string/feedback_email_warning_remote_action" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/email_warning_explanation" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <com.google.android.material.textfield.TextInputLayout android:id="@+id/feedback_type" style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:layout_marginTop="8dp" + android:layout_marginTop="16dp" android:layout_marginEnd="16dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/contact_separator"> + app:layout_constraintTop_toBottomOf="@+id/email_warning"> <AutoCompleteTextView android:id="@+id/feedback_type_entry" diff --git a/application/vlc-android/src/org/videolan/vlc/gui/FeedbackActivity.kt b/application/vlc-android/src/org/videolan/vlc/gui/FeedbackActivity.kt index 960c4a4a17b905ff06270400efef17380ede4435..c5614aa3ae4cf869993dbce36bbfc239850509e6 100644 --- a/application/vlc-android/src/org/videolan/vlc/gui/FeedbackActivity.kt +++ b/application/vlc-android/src/org/videolan/vlc/gui/FeedbackActivity.kt @@ -24,9 +24,11 @@ package org.videolan.vlc.gui +import android.content.Intent import android.os.Bundle import android.util.Log import android.view.MenuItem +import androidx.core.net.toUri import androidx.core.widget.addTextChangedListener import androidx.databinding.DataBindingUtil import androidx.lifecycle.lifecycleScope @@ -43,7 +45,9 @@ import org.videolan.resources.AppContextProvider import org.videolan.resources.CRASH_HAPPENED import org.videolan.resources.CRASH_ML_CTX import org.videolan.resources.CRASH_ML_MSG +import org.videolan.resources.TV_PREFERENCE_ACTIVITY import org.videolan.resources.util.applyOverscanMargin +import org.videolan.tools.Settings import org.videolan.tools.isVisible import org.videolan.tools.setGone import org.videolan.tools.setVisible @@ -53,6 +57,8 @@ import org.videolan.vlc.R import org.videolan.vlc.databinding.AboutFeedbackActivityBinding import org.videolan.vlc.gui.helpers.FeedbackUtil import org.videolan.vlc.gui.helpers.UiTools +import org.videolan.vlc.gui.preferences.EXTRA_PREF_END_POINT +import org.videolan.vlc.gui.preferences.PreferencesActivity import org.videolan.vlc.util.FileUtils import org.videolan.vlc.util.Permissions import org.videolan.vlc.util.TextUtils @@ -170,26 +176,41 @@ class FeedbackActivity : BaseActivity(), DebugLogService.Client.Callback { binding.feedbackTypeEntry.addTextChangedListener { updateFormIncludesVisibility() } + binding.feedbackTypeEntry.setOnClickListener { + binding.feedbackTypeEntry.showDropDown() + } binding.feedbackTypeEntry.setText(feedbackTypeEntries[0], false) + binding.emailWarningExplanation.text = getString(R.string.feedback_email_warning_explanation, getString(R.string.remote_access), getString(R.string.send_feedback)) + binding.tryAnyway.setOnClickListener { + binding.emailWarning.setGone() + switchFormVisibility() + updateFormIncludesVisibility() + } + binding.openSettings.setOnClickListener { + lifecycleScope.launch { + if (Settings.tvUI) { + val intent = Intent(Intent.ACTION_VIEW).setClassName(this@FeedbackActivity, TV_PREFERENCE_ACTIVITY) + intent.putExtra(EXTRA_PREF_END_POINT, "remote_access_category") + startActivity(intent) + } + else + PreferencesActivity.launchWithPref(this@FeedbackActivity, "enable_remote_access") + } + } binding.emailSupportCard.setOnClickListener { - if (binding.emailSupportForm.isVisible()) { - binding.emailSupportForm.setGone() - UiTools.setKeyboardVisibility(binding.messageTextInputLayout, false) - binding.emailSupportCard.nextFocusDownId = R.id.read_doc_card - binding.emailSupportCard.nextFocusRightId = R.id.read_doc_card + if (!isMailClientPresent()) { + switchNoEmailVisibility() } else { - binding.emailSupportForm.setVisible() - binding.emailSupportCard.nextFocusDownId = R.id.feedback_type_entry - binding.emailSupportCard.nextFocusRightId = R.id.feedback_type_entry + switchFormVisibility() + updateFormIncludesVisibility() } - updateFormIncludesVisibility() } binding.feedbackForumCard.setOnClickListener { - openLinkIfPossible("https://forum.videolan.org/viewforum.php?f=35") + openLinkIfPossible(getString(R.string.forum_url)) } binding.readDocCard.setOnClickListener { - openLinkIfPossible("https://docs.videolan.me/vlc-user/android/") + openLinkIfPossible(getString(R.string.doc_url)) } binding.emailSupportSend.setOnClickListener { if (binding.includeLogs.isChecked) { @@ -226,16 +247,46 @@ class FeedbackActivity : BaseActivity(), DebugLogService.Client.Callback { } } + private fun switchFormVisibility(forceHide: Boolean = false) { + if (forceHide || binding.emailSupportForm.isVisible()) { + binding.emailSupportForm.setGone() + UiTools.setKeyboardVisibility(binding.messageTextInputLayout, false) + binding.emailSupportCard.nextFocusDownId = R.id.read_doc_card + binding.emailSupportCard.nextFocusRightId = R.id.read_doc_card + } else { + binding.emailSupportForm.setVisible() + binding.emailSupportCard.nextFocusDownId = R.id.feedback_type_entry + binding.emailSupportCard.nextFocusRightId = R.id.feedback_type_entry + } + } + + private fun switchNoEmailVisibility() { + switchFormVisibility(true) + if (!binding.emailWarning.isVisible()) { + binding.emailWarning.setVisible() + binding.emailSupportCard.nextFocusDownId = R.id.open_settings + binding.emailSupportCard.nextFocusRightId = R.id.open_settings + } else { + binding.emailWarning.setGone() + binding.emailSupportCard.nextFocusDownId = R.id.read_doc_card + binding.emailSupportCard.nextFocusRightId = R.id.read_doc_card + } + } + + fun isMailClientPresent(): Boolean { + val intent = Intent(Intent.ACTION_SENDTO, "mailto:".toUri()) + val unsupportedActions = arrayOf("com.android.tv.frameworkpackagestubs", "com.google.android.tv.frameworkpackagestubs", "com.android.fallback") + val resolved = try { + intent.resolveActivity(packageManager) + } catch (e: Exception) { + return false + } + return resolved != null && resolved.packageName !in unsupportedActions + } + private fun sendEmail(includeLogs: Boolean = false) { val feedbackTypePosition = feedbackTypeEntries.indexOf(binding.feedbackTypeEntry.text.toString()) val isCrashFromML = !mlErrorContext.isNullOrEmpty() || !mlErrorMessage.isNullOrEmpty() - val subjectPrepend = when { - isCrashFromML -> "[ML Crash]" - feedbackTypePosition == 0 -> "[Help] " - feedbackTypePosition == 1 -> "[Feedback/Request] " - feedbackTypePosition == 2 -> "[Bug] " - else -> "[Crash] " - } val mail = if (BuildConfig.BETA && feedbackTypePosition > 2) FeedbackUtil.SupportType.CRASH_REPORT_EMAIL else FeedbackUtil.SupportType.SUPPORT_EMAIL lifecycleScope.launch { val message = if (isCrashFromML) @@ -248,15 +299,19 @@ class FeedbackActivity : BaseActivity(), DebugLogService.Client.Callback { append("ML Context: $mlErrorContext<br />ML error message: $mlErrorMessage") } else binding.messageTextInputLayout.editText?.text.toString() - FeedbackUtil.sendEmail( + if (!FeedbackUtil.sendEmail( this@FeedbackActivity, mail, binding.showIncludes && binding.includeMedialibrary.isChecked, message, - subjectPrepend + binding.subjectTextInputLayout.editText?.text.toString(), + binding.subjectTextInputLayout.editText?.text.toString(), + if (isCrashFromML) 100 else feedbackTypePosition, if (includeLogs) logcatZipPath else null - ) - finish() + )) { + UiTools.snacker(this@FeedbackActivity, R.string.feedback_email_warning) + switchNoEmailVisibility() + } else + finish() } } diff --git a/application/vlc-android/src/org/videolan/vlc/gui/helpers/FeedbackUtil.kt b/application/vlc-android/src/org/videolan/vlc/gui/helpers/FeedbackUtil.kt index da56fa776d54e15e56fdc91d1897e5a228da2e2f..eb685e990a50f9d1fa05d5281912a7cfdf412048 100644 --- a/application/vlc-android/src/org/videolan/vlc/gui/helpers/FeedbackUtil.kt +++ b/application/vlc-android/src/org/videolan/vlc/gui/helpers/FeedbackUtil.kt @@ -53,9 +53,11 @@ object FeedbackUtil { * @param includeMedialibrary Whether to include the medialibrary database or not * @param message Message to send * @param subject Subject of the email + * @param feedbackType Type of feedback * @param logcatZipPath Path to the logcat zip file + * @return true if the email was sent, false otherwise */ - suspend fun sendEmail(activity: FragmentActivity, supportType: SupportType, includeMedialibrary: Boolean, message: String, subject: String, logcatZipPath: String? = null) { + suspend fun sendEmail(activity: FragmentActivity, supportType: SupportType, includeMedialibrary: Boolean, message: String, subject: String, feedbackType: Int, logcatZipPath: String? = null): Boolean { val emailIntent = withContext(Dispatchers.IO) { val emailIntent = Intent(Intent.ACTION_SEND_MULTIPLE) @@ -87,12 +89,28 @@ object FeedbackUtil { val htmlBody = HtmlCompat.fromHtml(body, HtmlCompat.FROM_HTML_MODE_LEGACY) emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(supportType.email)) - emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject) + emailIntent.putExtra(Intent.EXTRA_SUBJECT, generateSubject(subject, feedbackType)) emailIntent.putExtra(Intent.EXTRA_TEXT, htmlBody) emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) emailIntent } - emailIntent?.let { activity.startActivity(it) } + try { + emailIntent?.let { activity.startActivity(it) } + return true + } catch (_: Exception) { + return false + } + } + + fun generateSubject(initialSubject: String, feedbackType: Int): String { + val subjectPrepend = when (feedbackType) { + 0 -> "[Help] " + 1 -> "[Feedback/Request] " + 2 -> "[Bug] " + 3 -> "[Crash] " + else -> "[ML Crash]" + } + return subjectPrepend + initialSubject } fun generateUsefulInfo(context: Context) = buildString { diff --git a/application/vlc-android/src/org/videolan/vlc/gui/helpers/UiTools.kt b/application/vlc-android/src/org/videolan/vlc/gui/helpers/UiTools.kt index 436be9b17a5e08bf0b2d2b2eb8f3cd688df89f66..c67f97cbe670bce18ece35fbbc3130022dac303f 100644 --- a/application/vlc-android/src/org/videolan/vlc/gui/helpers/UiTools.kt +++ b/application/vlc-android/src/org/videolan/vlc/gui/helpers/UiTools.kt @@ -513,9 +513,6 @@ object UiTools { v.findViewById<View>(R.id.about_website_container).setOnClickListener { activity.openLinkIfPossible("https://www.videolan.org/vlc/") } -// v.findViewById<View>(R.id.about_forum_container).setOnClickListener { -// activity.openLinkIfPossible("https://forum.videolan.org/viewforum.php?f=35") -// } v.findViewById<View>(R.id.about_report_container).setOnClickListener { activity.startActivity(Intent(activity, FeedbackActivity::class.java)) } diff --git a/build.gradle b/build.gradle index e84b88f9fbeaea1689c0f375134b648de8eb2cfc..7701812d45fe2b05d288464d418d5b73719829cb 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ ext { versionCode = 3060340 versionName = project.hasProperty('forceVlc4') && project.getProperty('forceVlc4') ? '4.0.0-preview - ' + versionCode : '3.6.4 Beta 4' vlcMajorVersion = project.hasProperty('forceVlc4') && project.getProperty('forceVlc4') ? 4 : 3 - remoteAccessVersion = '0.5.0' + remoteAccessVersion = '0.6.0' libvlcVersion = vlcMajorVersion == 3 ? '3.6.2' :'4.0.0-eap20' medialibraryVersion = vlcMajorVersion == 3 ? '0.13.13-rc17' : '0.13.13-vlc4-rc17' minSdkVersion = 17 diff --git a/buildsystem/compile-remoteaccess.sh b/buildsystem/compile-remoteaccess.sh index 81a716491451443bce163c921a53370da9534671..1e88d87dd2a9041daee99f44f746f82e745a2750 100755 --- a/buildsystem/compile-remoteaccess.sh +++ b/buildsystem/compile-remoteaccess.sh @@ -57,7 +57,7 @@ done ############################## diagnostic "Setting up the Remote Access project" - REMOTE_ACCESS_TESTED_HASH=a0fb5c209f506da0dd1cb1f9ff01b9f9a49eb489 + REMOTE_ACCESS_TESTED_HASH=bc7a94b38ca21739e21ba3f76dab09909d75a695 REMOTE_ACCESS_REPOSITORY=https://code.videolan.org/videolan/remoteaccess : ${VLC_REMOTE_ACCESS_PATH:="$(pwd -P)/application/remote-access-client/remoteaccess"}