Skip to content

Commit c7daf32

Browse files
authored
Merge pull request #7142 from litetex/better-player-error-handling
Better player error handling
2 parents ef91214 + 4c8dca5 commit c7daf32

12 files changed

Lines changed: 463 additions & 47 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package org.schabi.newpipe.error;
2+
3+
import android.util.Log;
4+
5+
import androidx.annotation.NonNull;
6+
7+
import java.io.ByteArrayOutputStream;
8+
import java.io.IOException;
9+
import java.io.ObjectOutputStream;
10+
import java.util.ArrayList;
11+
import java.util.Collections;
12+
import java.util.List;
13+
14+
/**
15+
* Ensures that a Exception is serializable.
16+
* This is
17+
*/
18+
public final class EnsureExceptionSerializable {
19+
private static final String TAG = "EnsureExSerializable";
20+
21+
private EnsureExceptionSerializable() {
22+
// No instance
23+
}
24+
25+
/**
26+
* Ensures that an exception is serializable.
27+
* <br/>
28+
* If that is not the case a {@link WorkaroundNotSerializableException} is created.
29+
*
30+
* @param exception
31+
* @return if an exception is not serializable a new {@link WorkaroundNotSerializableException}
32+
* otherwise the exception from the parameter
33+
*/
34+
public static Exception ensureSerializable(@NonNull final Exception exception) {
35+
return checkIfSerializable(exception)
36+
? exception
37+
: WorkaroundNotSerializableException.create(exception);
38+
}
39+
40+
public static boolean checkIfSerializable(@NonNull final Exception exception) {
41+
try {
42+
// Check by creating a new ObjectOutputStream which does the serialization
43+
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
44+
ObjectOutputStream oos = new ObjectOutputStream(bos)
45+
) {
46+
oos.writeObject(exception);
47+
oos.flush();
48+
49+
bos.toByteArray();
50+
}
51+
52+
return true;
53+
} catch (final IOException ex) {
54+
Log.d(TAG, "Exception is not serializable", ex);
55+
return false;
56+
}
57+
}
58+
59+
public static class WorkaroundNotSerializableException extends Exception {
60+
protected WorkaroundNotSerializableException(
61+
final Throwable notSerializableException,
62+
final Throwable cause) {
63+
super(notSerializableException.toString(), cause);
64+
setStackTrace(notSerializableException.getStackTrace());
65+
}
66+
67+
protected WorkaroundNotSerializableException(final Throwable notSerializableException) {
68+
super(notSerializableException.toString());
69+
setStackTrace(notSerializableException.getStackTrace());
70+
}
71+
72+
public static WorkaroundNotSerializableException create(
73+
@NonNull final Exception notSerializableException
74+
) {
75+
// Build a list of the exception + all causes
76+
final List<Throwable> throwableList = new ArrayList<>();
77+
78+
int pos = 0;
79+
Throwable throwableToProcess = notSerializableException;
80+
81+
while (throwableToProcess != null) {
82+
throwableList.add(throwableToProcess);
83+
84+
pos++;
85+
throwableToProcess = throwableToProcess.getCause();
86+
}
87+
88+
// Reverse list so that it starts with the last one
89+
Collections.reverse(throwableList);
90+
91+
// Build exception stack
92+
WorkaroundNotSerializableException cause = null;
93+
for (final Throwable t : throwableList) {
94+
cause = cause == null
95+
? new WorkaroundNotSerializableException(t)
96+
: new WorkaroundNotSerializableException(t, cause);
97+
}
98+
99+
return cause;
100+
}
101+
102+
}
103+
}

app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ public class ErrorActivity extends AppCompatActivity {
7777

7878
private ActivityErrorBinding activityErrorBinding;
7979

80+
/**
81+
* Reports a new error by starting a new activity.
82+
* <br/>
83+
* Ensure that the data within errorInfo is serializable otherwise
84+
* an exception will be thrown!<br/>
85+
* {@link EnsureExceptionSerializable} might help.
86+
*
87+
* @param context
88+
* @param errorInfo
89+
*/
8090
public static void reportError(final Context context, final ErrorInfo errorInfo) {
8191
final Intent intent = new Intent(context, ErrorActivity.class);
8292
intent.putExtra(ERROR_INFO, errorInfo);

app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,11 @@ private void toggleTitleAndSecondaryControls() {
594594
// Init
595595
//////////////////////////////////////////////////////////////////////////*/
596596

597+
@Override
598+
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
599+
super.onViewCreated(rootView, savedInstanceState);
600+
}
601+
597602
@Override // called from onViewCreated in {@link BaseFragment#onViewCreated}
598603
protected void initViews(final View rootView, final Bundle savedInstanceState) {
599604
super.initViews(rootView, savedInstanceState);
@@ -604,6 +609,18 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) {
604609

605610
binding.detailThumbnailRootLayout.requestFocus();
606611

612+
binding.detailControlsPlayWithKodi.setVisibility(
613+
KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)
614+
? View.VISIBLE
615+
: View.GONE
616+
);
617+
binding.detailControlsCrashThePlayer.setVisibility(
618+
DEBUG && PreferenceManager.getDefaultSharedPreferences(getContext())
619+
.getBoolean(getString(R.string.show_crash_the_player_key), false)
620+
? View.VISIBLE
621+
: View.GONE
622+
);
623+
607624
if (DeviceUtils.isTv(getContext())) {
608625
// remove ripple effects from detail controls
609626
final int transparent = ContextCompat.getColor(requireContext(),
@@ -638,8 +655,14 @@ protected void initListeners() {
638655
binding.detailControlsShare.setOnClickListener(this);
639656
binding.detailControlsOpenInBrowser.setOnClickListener(this);
640657
binding.detailControlsPlayWithKodi.setOnClickListener(this);
641-
binding.detailControlsPlayWithKodi.setVisibility(KoreUtils.shouldShowPlayWithKodi(
642-
requireContext(), serviceId) ? View.VISIBLE : View.GONE);
658+
if (DEBUG) {
659+
binding.detailControlsCrashThePlayer.setOnClickListener(
660+
v -> VideoDetailPlayerCrasher.onCrashThePlayer(
661+
this.getContext(),
662+
this.player,
663+
getLayoutInflater())
664+
);
665+
}
643666

644667
binding.overlayThumbnail.setOnClickListener(this);
645668
binding.overlayThumbnail.setOnLongClickListener(this);
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package org.schabi.newpipe.fragments.detail;
2+
3+
import android.content.Context;
4+
import android.util.Log;
5+
import android.view.ContextThemeWrapper;
6+
import android.view.LayoutInflater;
7+
import android.view.ViewGroup;
8+
import android.widget.RadioButton;
9+
import android.widget.RadioGroup;
10+
import android.widget.Toast;
11+
12+
import androidx.annotation.NonNull;
13+
import androidx.annotation.Nullable;
14+
import androidx.appcompat.app.AlertDialog;
15+
16+
import com.google.android.exoplayer2.C;
17+
import com.google.android.exoplayer2.ExoPlaybackException;
18+
19+
import org.schabi.newpipe.R;
20+
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
21+
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
22+
import org.schabi.newpipe.player.Player;
23+
import org.schabi.newpipe.util.ThemeHelper;
24+
25+
import java.io.IOException;
26+
import java.util.Collections;
27+
import java.util.LinkedHashMap;
28+
import java.util.Map;
29+
import java.util.function.Supplier;
30+
31+
/**
32+
* Outsourced logic for crashing the player in the {@link VideoDetailFragment}.
33+
*/
34+
public final class VideoDetailPlayerCrasher {
35+
36+
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
37+
// or it fails with an IllegalArgumentException
38+
// https://stackoverflow.com/a/54744028
39+
private static final String TAG = "VideoDetPlayerCrasher";
40+
41+
private static final Map<String, Supplier<ExoPlaybackException>> AVAILABLE_EXCEPTION_TYPES =
42+
getExceptionTypes();
43+
44+
private VideoDetailPlayerCrasher() {
45+
// No impls
46+
}
47+
48+
private static Map<String, Supplier<ExoPlaybackException>> getExceptionTypes() {
49+
final String defaultMsg = "Dummy";
50+
final Map<String, Supplier<ExoPlaybackException>> exceptionTypes = new LinkedHashMap<>();
51+
exceptionTypes.put(
52+
"Source",
53+
() -> ExoPlaybackException.createForSource(
54+
new IOException(defaultMsg)
55+
)
56+
);
57+
exceptionTypes.put(
58+
"Renderer",
59+
() -> ExoPlaybackException.createForRenderer(
60+
new Exception(defaultMsg),
61+
"Dummy renderer",
62+
0,
63+
null,
64+
C.FORMAT_HANDLED
65+
)
66+
);
67+
exceptionTypes.put(
68+
"Unexpected",
69+
() -> ExoPlaybackException.createForUnexpected(
70+
new RuntimeException(defaultMsg)
71+
)
72+
);
73+
exceptionTypes.put(
74+
"Remote",
75+
() -> ExoPlaybackException.createForRemote(defaultMsg)
76+
);
77+
78+
return Collections.unmodifiableMap(exceptionTypes);
79+
}
80+
81+
private static Context getThemeWrapperContext(final Context context) {
82+
return new ContextThemeWrapper(
83+
context,
84+
ThemeHelper.isLightThemeSelected(context)
85+
? R.style.LightTheme
86+
: R.style.DarkTheme);
87+
}
88+
89+
public static void onCrashThePlayer(
90+
@NonNull final Context context,
91+
@Nullable final Player player,
92+
@NonNull final LayoutInflater layoutInflater
93+
) {
94+
if (player == null) {
95+
Log.d(TAG, "Player is not available");
96+
Toast.makeText(context, "Player is not available", Toast.LENGTH_SHORT)
97+
.show();
98+
99+
return;
100+
}
101+
102+
// -- Build the dialog/UI --
103+
104+
final Context themeWrapperContext = getThemeWrapperContext(context);
105+
106+
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
107+
final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(layoutInflater)
108+
.list;
109+
110+
final AlertDialog alertDialog = new AlertDialog.Builder(getThemeWrapperContext(context))
111+
.setTitle("Choose an exception")
112+
.setView(radioGroup)
113+
.setCancelable(true)
114+
.setNegativeButton(R.string.cancel, null)
115+
.create();
116+
117+
for (final Map.Entry<String, Supplier<ExoPlaybackException>> entry
118+
: AVAILABLE_EXCEPTION_TYPES.entrySet()) {
119+
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
120+
radioButton.setText(entry.getKey());
121+
radioButton.setChecked(false);
122+
radioButton.setLayoutParams(
123+
new RadioGroup.LayoutParams(
124+
ViewGroup.LayoutParams.MATCH_PARENT,
125+
ViewGroup.LayoutParams.WRAP_CONTENT
126+
)
127+
);
128+
radioButton.setOnClickListener(v -> {
129+
tryCrashPlayerWith(player, entry.getValue().get());
130+
if (alertDialog != null) {
131+
alertDialog.cancel();
132+
}
133+
});
134+
radioGroup.addView(radioButton);
135+
}
136+
137+
alertDialog.show();
138+
}
139+
140+
/**
141+
* Note that this method does not crash the underlying exoplayer directly (it's not possible).
142+
* It simply supplies a Exception to {@link Player#onPlayerError(ExoPlaybackException)}.
143+
* @param player
144+
* @param exception
145+
*/
146+
private static void tryCrashPlayerWith(
147+
@NonNull final Player player,
148+
@NonNull final ExoPlaybackException exception
149+
) {
150+
Log.d(TAG, "Crashing the player using player.onPlayerError(ex)");
151+
try {
152+
player.onPlayerError(exception);
153+
} catch (final Exception exPlayer) {
154+
Log.e(TAG,
155+
"Run into an exception while crashing the player:",
156+
exPlayer);
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)