Merge pull request #7142 from litetex/better-player-error-handling
Better player error handling
This commit is contained in:
commit
c7daf32904
|
@ -0,0 +1,103 @@
|
|||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ensures that a Exception is serializable.
|
||||
* This is
|
||||
*/
|
||||
public final class EnsureExceptionSerializable {
|
||||
private static final String TAG = "EnsureExSerializable";
|
||||
|
||||
private EnsureExceptionSerializable() {
|
||||
// No instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an exception is serializable.
|
||||
* <br/>
|
||||
* If that is not the case a {@link WorkaroundNotSerializableException} is created.
|
||||
*
|
||||
* @param exception
|
||||
* @return if an exception is not serializable a new {@link WorkaroundNotSerializableException}
|
||||
* otherwise the exception from the parameter
|
||||
*/
|
||||
public static Exception ensureSerializable(@NonNull final Exception exception) {
|
||||
return checkIfSerializable(exception)
|
||||
? exception
|
||||
: WorkaroundNotSerializableException.create(exception);
|
||||
}
|
||||
|
||||
public static boolean checkIfSerializable(@NonNull final Exception exception) {
|
||||
try {
|
||||
// Check by creating a new ObjectOutputStream which does the serialization
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(bos)
|
||||
) {
|
||||
oos.writeObject(exception);
|
||||
oos.flush();
|
||||
|
||||
bos.toByteArray();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (final IOException ex) {
|
||||
Log.d(TAG, "Exception is not serializable", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class WorkaroundNotSerializableException extends Exception {
|
||||
protected WorkaroundNotSerializableException(
|
||||
final Throwable notSerializableException,
|
||||
final Throwable cause) {
|
||||
super(notSerializableException.toString(), cause);
|
||||
setStackTrace(notSerializableException.getStackTrace());
|
||||
}
|
||||
|
||||
protected WorkaroundNotSerializableException(final Throwable notSerializableException) {
|
||||
super(notSerializableException.toString());
|
||||
setStackTrace(notSerializableException.getStackTrace());
|
||||
}
|
||||
|
||||
public static WorkaroundNotSerializableException create(
|
||||
@NonNull final Exception notSerializableException
|
||||
) {
|
||||
// Build a list of the exception + all causes
|
||||
final List<Throwable> throwableList = new ArrayList<>();
|
||||
|
||||
int pos = 0;
|
||||
Throwable throwableToProcess = notSerializableException;
|
||||
|
||||
while (throwableToProcess != null) {
|
||||
throwableList.add(throwableToProcess);
|
||||
|
||||
pos++;
|
||||
throwableToProcess = throwableToProcess.getCause();
|
||||
}
|
||||
|
||||
// Reverse list so that it starts with the last one
|
||||
Collections.reverse(throwableList);
|
||||
|
||||
// Build exception stack
|
||||
WorkaroundNotSerializableException cause = null;
|
||||
for (final Throwable t : throwableList) {
|
||||
cause = cause == null
|
||||
? new WorkaroundNotSerializableException(t)
|
||||
: new WorkaroundNotSerializableException(t, cause);
|
||||
}
|
||||
|
||||
return cause;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -77,6 +77,16 @@ public class ErrorActivity extends AppCompatActivity {
|
|||
|
||||
private ActivityErrorBinding activityErrorBinding;
|
||||
|
||||
/**
|
||||
* Reports a new error by starting a new activity.
|
||||
* <br/>
|
||||
* Ensure that the data within errorInfo is serializable otherwise
|
||||
* an exception will be thrown!<br/>
|
||||
* {@link EnsureExceptionSerializable} might help.
|
||||
*
|
||||
* @param context
|
||||
* @param errorInfo
|
||||
*/
|
||||
public static void reportError(final Context context, final ErrorInfo errorInfo) {
|
||||
final Intent intent = new Intent(context, ErrorActivity.class);
|
||||
intent.putExtra(ERROR_INFO, errorInfo);
|
||||
|
|
|
@ -594,6 +594,11 @@ public final class VideoDetailFragment
|
|||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override // called from onViewCreated in {@link BaseFragment#onViewCreated}
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
@ -604,6 +609,18 @@ public final class VideoDetailFragment
|
|||
|
||||
binding.detailThumbnailRootLayout.requestFocus();
|
||||
|
||||
binding.detailControlsPlayWithKodi.setVisibility(
|
||||
KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)
|
||||
? View.VISIBLE
|
||||
: View.GONE
|
||||
);
|
||||
binding.detailControlsCrashThePlayer.setVisibility(
|
||||
DEBUG && PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
.getBoolean(getString(R.string.show_crash_the_player_key), false)
|
||||
? View.VISIBLE
|
||||
: View.GONE
|
||||
);
|
||||
|
||||
if (DeviceUtils.isTv(getContext())) {
|
||||
// remove ripple effects from detail controls
|
||||
final int transparent = ContextCompat.getColor(requireContext(),
|
||||
|
@ -638,8 +655,14 @@ public final class VideoDetailFragment
|
|||
binding.detailControlsShare.setOnClickListener(this);
|
||||
binding.detailControlsOpenInBrowser.setOnClickListener(this);
|
||||
binding.detailControlsPlayWithKodi.setOnClickListener(this);
|
||||
binding.detailControlsPlayWithKodi.setVisibility(KoreUtils.shouldShowPlayWithKodi(
|
||||
requireContext(), serviceId) ? View.VISIBLE : View.GONE);
|
||||
if (DEBUG) {
|
||||
binding.detailControlsCrashThePlayer.setOnClickListener(
|
||||
v -> VideoDetailPlayerCrasher.onCrashThePlayer(
|
||||
this.getContext(),
|
||||
this.player,
|
||||
getLayoutInflater())
|
||||
);
|
||||
}
|
||||
|
||||
binding.overlayThumbnail.setOnClickListener(this);
|
||||
binding.overlayThumbnail.setOnLongClickListener(this);
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
|
||||
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Outsourced logic for crashing the player in the {@link VideoDetailFragment}.
|
||||
*/
|
||||
public final class VideoDetailPlayerCrasher {
|
||||
|
||||
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
|
||||
// or it fails with an IllegalArgumentException
|
||||
// https://stackoverflow.com/a/54744028
|
||||
private static final String TAG = "VideoDetPlayerCrasher";
|
||||
|
||||
private static final Map<String, Supplier<ExoPlaybackException>> AVAILABLE_EXCEPTION_TYPES =
|
||||
getExceptionTypes();
|
||||
|
||||
private VideoDetailPlayerCrasher() {
|
||||
// No impls
|
||||
}
|
||||
|
||||
private static Map<String, Supplier<ExoPlaybackException>> getExceptionTypes() {
|
||||
final String defaultMsg = "Dummy";
|
||||
final Map<String, Supplier<ExoPlaybackException>> exceptionTypes = new LinkedHashMap<>();
|
||||
exceptionTypes.put(
|
||||
"Source",
|
||||
() -> ExoPlaybackException.createForSource(
|
||||
new IOException(defaultMsg)
|
||||
)
|
||||
);
|
||||
exceptionTypes.put(
|
||||
"Renderer",
|
||||
() -> ExoPlaybackException.createForRenderer(
|
||||
new Exception(defaultMsg),
|
||||
"Dummy renderer",
|
||||
0,
|
||||
null,
|
||||
C.FORMAT_HANDLED
|
||||
)
|
||||
);
|
||||
exceptionTypes.put(
|
||||
"Unexpected",
|
||||
() -> ExoPlaybackException.createForUnexpected(
|
||||
new RuntimeException(defaultMsg)
|
||||
)
|
||||
);
|
||||
exceptionTypes.put(
|
||||
"Remote",
|
||||
() -> ExoPlaybackException.createForRemote(defaultMsg)
|
||||
);
|
||||
|
||||
return Collections.unmodifiableMap(exceptionTypes);
|
||||
}
|
||||
|
||||
private static Context getThemeWrapperContext(final Context context) {
|
||||
return new ContextThemeWrapper(
|
||||
context,
|
||||
ThemeHelper.isLightThemeSelected(context)
|
||||
? R.style.LightTheme
|
||||
: R.style.DarkTheme);
|
||||
}
|
||||
|
||||
public static void onCrashThePlayer(
|
||||
@NonNull final Context context,
|
||||
@Nullable final Player player,
|
||||
@NonNull final LayoutInflater layoutInflater
|
||||
) {
|
||||
if (player == null) {
|
||||
Log.d(TAG, "Player is not available");
|
||||
Toast.makeText(context, "Player is not available", Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// -- Build the dialog/UI --
|
||||
|
||||
final Context themeWrapperContext = getThemeWrapperContext(context);
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||
final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(layoutInflater)
|
||||
.list;
|
||||
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(getThemeWrapperContext(context))
|
||||
.setTitle("Choose an exception")
|
||||
.setView(radioGroup)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
for (final Map.Entry<String, Supplier<ExoPlaybackException>> entry
|
||||
: AVAILABLE_EXCEPTION_TYPES.entrySet()) {
|
||||
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
|
||||
radioButton.setText(entry.getKey());
|
||||
radioButton.setChecked(false);
|
||||
radioButton.setLayoutParams(
|
||||
new RadioGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
);
|
||||
radioButton.setOnClickListener(v -> {
|
||||
tryCrashPlayerWith(player, entry.getValue().get());
|
||||
if (alertDialog != null) {
|
||||
alertDialog.cancel();
|
||||
}
|
||||
});
|
||||
radioGroup.addView(radioButton);
|
||||
}
|
||||
|
||||
alertDialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that this method does not crash the underlying exoplayer directly (it's not possible).
|
||||
* It simply supplies a Exception to {@link Player#onPlayerError(ExoPlaybackException)}.
|
||||
* @param player
|
||||
* @param exception
|
||||
*/
|
||||
private static void tryCrashPlayerWith(
|
||||
@NonNull final Player player,
|
||||
@NonNull final ExoPlaybackException exception
|
||||
) {
|
||||
Log.d(TAG, "Crashing the player using player.onPlayerError(ex)");
|
||||
try {
|
||||
player.onPlayerError(exception);
|
||||
} catch (final Exception exPlayer) {
|
||||
Log.e(TAG,
|
||||
"Run into an exception while crashing the player:",
|
||||
exPlayer);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -97,7 +97,6 @@ import android.widget.ProgressBar;
|
|||
import android.widget.RelativeLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -166,6 +165,7 @@ import org.schabi.newpipe.player.playback.MediaSourceManager;
|
|||
import org.schabi.newpipe.player.playback.PlaybackListener;
|
||||
import org.schabi.newpipe.player.playback.PlayerMediaSession;
|
||||
import org.schabi.newpipe.player.playback.SurfaceHolderCallback;
|
||||
import org.schabi.newpipe.player.playererror.PlayerErrorHandler;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
@ -268,7 +268,7 @@ public final class Player implements
|
|||
@Nullable private MediaSourceTag currentMetadata;
|
||||
@Nullable private Bitmap currentThumbnail;
|
||||
|
||||
@Nullable private Toast errorToast;
|
||||
@NonNull private PlayerErrorHandler playerErrorHandler;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player
|
||||
|
@ -413,6 +413,8 @@ public final class Player implements
|
|||
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
|
||||
audioResolver = new AudioPlaybackResolver(context, dataSource);
|
||||
|
||||
playerErrorHandler = new PlayerErrorHandler(context);
|
||||
|
||||
windowManager = ContextCompat.getSystemService(context, WindowManager.class);
|
||||
}
|
||||
|
||||
|
@ -2512,30 +2514,33 @@ public final class Player implements
|
|||
*/
|
||||
@Override
|
||||
public void onPlayerError(@NonNull final ExoPlaybackException error) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "ExoPlayer - onPlayerError() called with: " + "error = [" + error + "]");
|
||||
}
|
||||
if (errorToast != null) {
|
||||
errorToast.cancel();
|
||||
errorToast = null;
|
||||
}
|
||||
Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error);
|
||||
|
||||
saveStreamProgressState();
|
||||
|
||||
switch (error.type) {
|
||||
case ExoPlaybackException.TYPE_SOURCE:
|
||||
processSourceError(error.getSourceException());
|
||||
showStreamError(error);
|
||||
playerErrorHandler.showPlayerError(
|
||||
error,
|
||||
currentMetadata.getMetadata(),
|
||||
R.string.player_stream_failure);
|
||||
break;
|
||||
case ExoPlaybackException.TYPE_UNEXPECTED:
|
||||
showRecoverableError(error);
|
||||
playerErrorHandler.showPlayerError(
|
||||
error,
|
||||
currentMetadata.getMetadata(),
|
||||
R.string.player_recoverable_failure);
|
||||
setRecovery();
|
||||
reloadPlayQueueManager();
|
||||
break;
|
||||
case ExoPlaybackException.TYPE_REMOTE:
|
||||
case ExoPlaybackException.TYPE_RENDERER:
|
||||
default:
|
||||
showUnrecoverableError(error);
|
||||
playerErrorHandler.showPlayerError(
|
||||
error,
|
||||
currentMetadata.getMetadata(),
|
||||
R.string.player_unrecoverable_failure);
|
||||
onPlaybackShutdown();
|
||||
break;
|
||||
}
|
||||
|
@ -2557,37 +2562,6 @@ public final class Player implements
|
|||
playQueue.error();
|
||||
}
|
||||
}
|
||||
|
||||
private void showStreamError(final Exception exception) {
|
||||
exception.printStackTrace();
|
||||
|
||||
if (errorToast == null) {
|
||||
errorToast = Toast
|
||||
.makeText(context, R.string.player_stream_failure, Toast.LENGTH_SHORT);
|
||||
errorToast.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void showRecoverableError(final Exception exception) {
|
||||
exception.printStackTrace();
|
||||
|
||||
if (errorToast == null) {
|
||||
errorToast = Toast
|
||||
.makeText(context, R.string.player_recoverable_failure, Toast.LENGTH_SHORT);
|
||||
errorToast.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void showUnrecoverableError(final Exception exception) {
|
||||
exception.printStackTrace();
|
||||
|
||||
if (errorToast != null) {
|
||||
errorToast.cancel();
|
||||
}
|
||||
errorToast = Toast
|
||||
.makeText(context, R.string.player_unrecoverable_failure, Toast.LENGTH_SHORT);
|
||||
errorToast.show();
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
package org.schabi.newpipe.player.playererror;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.EnsureExceptionSerializable;
|
||||
import org.schabi.newpipe.error.ErrorActivity;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
|
||||
/**
|
||||
* Handles (exoplayer)errors that occur in the player.
|
||||
*/
|
||||
public class PlayerErrorHandler {
|
||||
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
|
||||
// or it fails with an IllegalArgumentException
|
||||
// https://stackoverflow.com/a/54744028
|
||||
private static final String TAG = "PlayerErrorHandler";
|
||||
|
||||
@Nullable
|
||||
private Toast errorToast;
|
||||
|
||||
@NonNull
|
||||
private final Context context;
|
||||
|
||||
public PlayerErrorHandler(@NonNull final Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void showPlayerError(
|
||||
@NonNull final ExoPlaybackException exception,
|
||||
@NonNull final Info info,
|
||||
@StringRes final int textResId
|
||||
) {
|
||||
// Hide existing toast message
|
||||
if (errorToast != null) {
|
||||
Log.d(TAG, "Trying to cancel previous player error error toast");
|
||||
errorToast.cancel();
|
||||
errorToast = null;
|
||||
}
|
||||
|
||||
if (shouldReportError()) {
|
||||
try {
|
||||
reportError(exception, info);
|
||||
// When a report pops up we need no toast
|
||||
return;
|
||||
} catch (final Exception ex) {
|
||||
Log.w(TAG, "Unable to report error:", ex);
|
||||
// This will show the toast as fallback
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Showing player error toast");
|
||||
errorToast = Toast.makeText(context, textResId, Toast.LENGTH_SHORT);
|
||||
errorToast.show();
|
||||
}
|
||||
|
||||
private void reportError(@NonNull final ExoPlaybackException exception,
|
||||
@NonNull final Info info) {
|
||||
ErrorActivity.reportError(
|
||||
context,
|
||||
new ErrorInfo(
|
||||
EnsureExceptionSerializable.ensureSerializable(exception),
|
||||
UserAction.PLAY_STREAM,
|
||||
"Player error[type=" + exception.type + "] occurred while playing: "
|
||||
+ info.getUrl(),
|
||||
info
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private boolean shouldReportError() {
|
||||
return PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getBoolean(
|
||||
context.getString(R.string.report_player_errors_key),
|
||||
false);
|
||||
}
|
||||
}
|
|
@ -225,7 +225,7 @@
|
|||
android:layout_below="@id/detail_title_root_layout"
|
||||
android:layout_marginTop="@dimen/video_item_detail_error_panel_margin"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
tools:visibility="gone" />
|
||||
|
||||
<!--HIDING ROOT-->
|
||||
<LinearLayout
|
||||
|
@ -563,6 +563,22 @@
|
|||
android:textSize="@dimen/detail_control_text_size"
|
||||
app:drawableTopCompat="@drawable/ic_cast" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/detail_controls_crash_the_player"
|
||||
android:layout_width="@dimen/detail_control_width"
|
||||
android:layout_height="@dimen/detail_control_height"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/crash_the_player"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:paddingVertical="@dimen/detail_control_padding"
|
||||
android:text="@string/crash_the_player"
|
||||
android:textSize="@dimen/detail_control_text_size"
|
||||
app:drawableTopCompat="@drawable/ic_bug_report" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
|
|
|
@ -213,7 +213,7 @@
|
|||
android:layout_below="@id/detail_title_root_layout"
|
||||
android:layout_marginTop="@dimen/video_item_detail_error_panel_margin"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
tools:visibility="gone" />
|
||||
|
||||
<!--HIDING ROOT-->
|
||||
<LinearLayout
|
||||
|
@ -547,6 +547,22 @@
|
|||
android:textSize="@dimen/detail_control_text_size"
|
||||
app:drawableTopCompat="@drawable/ic_cast" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/detail_controls_crash_the_player"
|
||||
android:layout_width="@dimen/detail_control_width"
|
||||
android:layout_height="@dimen/detail_control_height"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/crash_the_player"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:paddingVertical="@dimen/detail_control_padding"
|
||||
android:text="@string/crash_the_player"
|
||||
android:textSize="@dimen/detail_control_text_size"
|
||||
app:drawableTopCompat="@drawable/ic_bug_report" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
|
|
|
@ -89,6 +89,8 @@
|
|||
<item>@string/never</item>
|
||||
</string-array>
|
||||
|
||||
<string name="report_player_errors_key" translatable="false">report_player_errors_key</string>
|
||||
|
||||
<string name="seekbar_preview_thumbnail_key" translatable="false">seekbar_preview_thumbnail_key</string>
|
||||
<string name="seekbar_preview_thumbnail_high_quality" translatable="false">seekbar_preview_thumbnail_high_quality</string>
|
||||
<string name="seekbar_preview_thumbnail_low_quality" translatable="false">seekbar_preview_thumbnail_low_quality</string>
|
||||
|
@ -188,6 +190,7 @@
|
|||
<string name="disable_media_tunneling_key" translatable="false">disable_media_tunneling_key</string>
|
||||
<string name="crash_the_app_key" translatable="false">crash_the_app_key</string>
|
||||
<string name="show_image_indicators_key" translatable="false">show_image_indicators_key</string>
|
||||
<string name="show_crash_the_player_key" translatable="false">show_crash_the_player_key</string>
|
||||
|
||||
<!-- THEMES -->
|
||||
<string name="theme_key" translatable="false">theme</string>
|
||||
|
|
|
@ -52,6 +52,9 @@
|
|||
<string name="kore_package" translatable="false">org.xbmc.kore</string>
|
||||
<string name="show_play_with_kodi_title">Show \"Play with Kodi\" option</string>
|
||||
<string name="show_play_with_kodi_summary">Display an option to play a video via Kodi media center</string>
|
||||
<string name="crash_the_player">Crash the player</string>
|
||||
<string name="report_player_errors_title">Report player errors</string>
|
||||
<string name="report_player_errors_summary">Reports player errors in full detail instead of showing a short-lived toast message (useful for diagnosing problems)</string>
|
||||
<string name="notification_scale_to_square_image_title">Scale thumbnail to 1:1 aspect ratio</string>
|
||||
<string name="notification_scale_to_square_image_summary">Scale the video thumbnail shown in the notification from 16:9 to 1:1 aspect ratio (may introduce distortions)</string>
|
||||
<string name="notification_action_0_title">First action button</string>
|
||||
|
@ -473,6 +476,8 @@
|
|||
<string name="show_image_indicators_title">Show image indicators</string>
|
||||
<string name="show_image_indicators_summary">Show Picasso colored ribbons on top of images indicating their source: red for network, blue for disk and green for memory</string>
|
||||
<string name="crash_the_app">Crash the app</string>
|
||||
<string name="show_crash_the_player_title">Show \"crash the player\"</string>
|
||||
<string name="show_crash_the_player_summary">Shows a crash option when using the player</string>
|
||||
<!-- Subscriptions import/export -->
|
||||
<string name="import_title">Import</string>
|
||||
<string name="import_from">Import from</string>
|
||||
|
|
|
@ -49,9 +49,26 @@
|
|||
android:title="@string/show_image_indicators_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="@string/report_player_errors_key"
|
||||
android:summary="@string/report_player_errors_summary"
|
||||
android:title="@string/report_player_errors_title"
|
||||
app:singleLineTitle="false"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<Preference
|
||||
android:key="@string/crash_the_app_key"
|
||||
android:title="@string/crash_the_app"
|
||||
app:singleLineTitle="false"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:defaultValue="false"
|
||||
android:key="@string/show_crash_the_player_key"
|
||||
android:summary="@string/show_crash_the_player_summary"
|
||||
android:title="@string/show_crash_the_player_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceScreen>
|
||||
|
|
|
@ -89,6 +89,7 @@
|
|||
android:title="@string/show_play_with_kodi_title"
|
||||
app:singleLineTitle="false"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<ListPreference
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
Loading…
Reference in New Issue