Private
Public Access
Merge pull request 'Minamp 1.9: playback fixes for Bluetooth and USB' (#2) from ios-backports into main
This commit was merged in pull request #2.
This commit is contained in:
commit
9aaeb57c87
8 files changed
+91
-25
No files matched your search
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.minamp"
|
package="com.minamp"
|
||||||
android:versionCode="9"
|
android:versionCode="10"
|
||||||
android:versionName="1.8"
|
android:versionName="1.9"
|
||||||
android:installLocation="auto">
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
|
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" />
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ log stays in English since it's meant for troubleshooting.
|
|||||||
1. On the phone: **Settings → Applications → Unknown sources** (tick it).
|
1. On the phone: **Settings → Applications → Unknown sources** (tick it).
|
||||||
2. Copy `Minamp.apk` to the SD card (USB cable, or download it in the browser) and open it
|
2. Copy `Minamp.apk` to the SD card (USB cable, or download it in the browser) and open it
|
||||||
with a file manager, or from a computer: `adb install Minamp.apk`.
|
with a file manager, or from a computer: `adb install Minamp.apk`.
|
||||||
3. The app can live on the SD card (Settings → Applications → Manage → Minamp → Move to SD card),
|
3. Minamp installs to internal storage (about 1 MB): an app on the SD card is killed whenever the
|
||||||
which helps with the Mini's tiny internal storage.
|
phone shares its SD card over USB, which stopped the music.
|
||||||
|
|
||||||
## Sign in
|
## Sign in
|
||||||
|
|
||||||
@@ -56,11 +56,16 @@ clock has been reset to before 2024 the certificate date check is skipped rather
|
|||||||
* MP3, AAC (M4A) and Ogg Vorbis play directly. FLAC, ALAC etc. are transcoded to MP3 by the
|
* MP3, AAC (M4A) and Ogg Vorbis play directly. FLAC, ALAC etc. are transcoded to MP3 by the
|
||||||
server (Android 2.2/2.3 can't decode FLAC).
|
server (Android 2.2/2.3 can't decode FLAC).
|
||||||
* **Menu → Settings**: separate quality for home Wi-Fi (default *Original*) and for away/mobile
|
* **Menu → Settings**: separate quality for home Wi-Fi (default *Original*) and for away/mobile
|
||||||
data (default *192 kbps MP3*); 320/256/192/128/96 kbps available.
|
data (default *192 kbps MP3*); 320/256/192/128/96 kbps available. *Quality over Bluetooth*
|
||||||
|
(default 192 kbps) caps both while the sound goes to a Bluetooth speaker: on the Mini, Wi-Fi and
|
||||||
|
Bluetooth share one radio, and a lighter stream keeps the speaker from stuttering.
|
||||||
* Seeking in a transcoded track restarts the transcode at the new position.
|
* Seeking in a transcoded track restarts the transcode at the new position.
|
||||||
* If a stream fails, Minamp re-picks the connection and resumes; if it fails again it downloads
|
* If a stream fails, Minamp re-picks the connection and resumes; if it fails again it downloads
|
||||||
the whole track first and plays from the SD card. "Download tracks before playing" forces that
|
the whole track first and plays from the SD card. "Download tracks before playing" forces that
|
||||||
mode (useful on flaky mobile data).
|
mode (useful on flaky mobile data). Playback that stalls ("playing" but the time doesn't move
|
||||||
|
for 10 s) is recovered the same way.
|
||||||
|
* A Bluetooth speaker that drops its link for a moment pauses Minamp, which resumes by itself if the
|
||||||
|
speaker is back within 20 s. Why playback paused or stopped is written to the connection log.
|
||||||
* Plays are reported to Plex (Now Playing in the Plex dashboard, play counts, resume position).
|
* Plays are reported to Plex (Now Playing in the Plex dashboard, play counts, resume position).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|||||||
@@ -199,7 +199,7 @@
|
|||||||
<string name="sort_played">Most played</string>
|
<string name="sort_played">Most played</string>
|
||||||
<string name="sort_title">Title</string>
|
<string name="sort_title">Title</string>
|
||||||
<string name="sort_year">Year (newest)</string>
|
<string name="sort_year">Year (newest)</string>
|
||||||
<string name="stalled">Playback is stuck. Try pausing and playing again.</string>
|
<string name="stalled">Playback got stuck: the network isn\'t keeping up.</string>
|
||||||
<string name="starting_x">Starting %1$s…</string>
|
<string name="starting_x">Starting %1$s…</string>
|
||||||
<string name="stream_interrupted">Stream interrupted</string>
|
<string name="stream_interrupted">Stream interrupted</string>
|
||||||
<string name="surprise_me">Surprise me</string>
|
<string name="surprise_me">Surprise me</string>
|
||||||
@@ -236,4 +236,6 @@
|
|||||||
<string name="unloved">Removed from favourites</string>
|
<string name="unloved">Removed from favourites</string>
|
||||||
<string name="up_next">Queue</string>
|
<string name="up_next">Queue</string>
|
||||||
<string name="you">you</string>
|
<string name="you">you</string>
|
||||||
|
<string name="quality_bt_title">Quality over Bluetooth</string>
|
||||||
|
<string name="set_quality_bt">Quality over Bluetooth: %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
<string name="sort_played">Máis escoitados</string>
|
<string name="sort_played">Máis escoitados</string>
|
||||||
<string name="sort_title">Título</string>
|
<string name="sort_title">Título</string>
|
||||||
<string name="sort_year">Ano (máis recentes)</string>
|
<string name="sort_year">Ano (máis recentes)</string>
|
||||||
<string name="stalled">A reprodución quedou parada. Proba a pausar e reproducir de novo.</string>
|
<string name="stalled">A reprodución quedou parada: a rede non dá abasto.</string>
|
||||||
<string name="starting_x">Iniciando %1$s…</string>
|
<string name="starting_x">Iniciando %1$s…</string>
|
||||||
<string name="stream_interrupted">Interrompeuse a transmisión</string>
|
<string name="stream_interrupted">Interrompeuse a transmisión</string>
|
||||||
<string name="surprise_me">Sorpréndeme</string>
|
<string name="surprise_me">Sorpréndeme</string>
|
||||||
@@ -236,4 +236,6 @@
|
|||||||
<string name="unloved">Quitada de favoritas</string>
|
<string name="unloved">Quitada de favoritas</string>
|
||||||
<string name="up_next">Cola</string>
|
<string name="up_next">Cola</string>
|
||||||
<string name="you">ti</string>
|
<string name="you">ti</string>
|
||||||
|
<string name="quality_bt_title">Calidade por Bluetooth</string>
|
||||||
|
<string name="set_quality_bt">Calidade por Bluetooth: %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -296,12 +296,20 @@ public class App extends Application {
|
|||||||
|
|
||||||
public int qualityHome() { return prefs.getInt("qHome", 0); }
|
public int qualityHome() { return prefs.getInt("qHome", 0); }
|
||||||
public int qualityAway() { return prefs.getInt("qAway", 192); }
|
public int qualityAway() { return prefs.getInt("qAway", 192); }
|
||||||
|
/** Cap while the sound goes to a Bluetooth speaker (0 = none). Wi-Fi and Bluetooth share this phone's radio:
|
||||||
|
* less to download leaves the Bluetooth link the airtime it needs, so it doesn't stutter. */
|
||||||
|
public int qualityBluetooth() { return prefs.getInt("qBt", 192); }
|
||||||
|
|
||||||
/** Max kbps for the current situation; 0 = original. */
|
/** Max kbps for the current situation; 0 = original. */
|
||||||
public int currentMaxKbps() {
|
public int currentMaxKbps() {
|
||||||
boolean away = server == null || !server.isLocal() || onMobileData();
|
boolean away = server == null || !server.isLocal() || onMobileData();
|
||||||
int q = away ? qualityAway() : qualityHome();
|
int q = away ? qualityAway() : qualityHome();
|
||||||
if (server != null && server.isRelay() && (q == 0 || q > 192)) q = 192; // relay is bandwidth-capped
|
if (server != null && server.isRelay() && (q == 0 || q > 192)) q = 192; // relay is bandwidth-capped
|
||||||
|
int bt = qualityBluetooth();
|
||||||
|
if (bt > 0 && (q == 0 || q > bt)) {
|
||||||
|
android.media.AudioManager am = (android.media.AudioManager) getSystemService(AUDIO_SERVICE);
|
||||||
|
if (am != null && am.isBluetoothA2dpOn()) q = bt;
|
||||||
|
}
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -435,6 +435,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
|||||||
final String[] rows = {
|
final String[] rows = {
|
||||||
str(R.string.set_quality_home, qualityLabel(app.qualityHome())),
|
str(R.string.set_quality_home, qualityLabel(app.qualityHome())),
|
||||||
str(R.string.set_quality_away, qualityLabel(app.qualityAway())),
|
str(R.string.set_quality_away, qualityLabel(app.qualityAway())),
|
||||||
|
str(R.string.set_quality_bt, qualityLabel(app.qualityBluetooth())),
|
||||||
str(R.string.set_dl_quality, dlQualityLabel(Downloads.get().quality())),
|
str(R.string.set_dl_quality, dlQualityLabel(Downloads.get().quality())),
|
||||||
str(R.string.set_delete_downloads, Downloads.size(Downloads.get().bytesOnDisk())),
|
str(R.string.set_delete_downloads, Downloads.size(Downloads.get().bytesOnDisk())),
|
||||||
str(R.string.set_download_first, str(app.downloadFirst() ? R.string.on : R.string.off)),
|
str(R.string.set_download_first, str(app.downloadFirst() ? R.string.on : R.string.off)),
|
||||||
@@ -454,21 +455,22 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
|||||||
switch (which) {
|
switch (which) {
|
||||||
case 0: pickQuality("qHome", app.qualityHome(), str(R.string.quality_home_title)); break;
|
case 0: pickQuality("qHome", app.qualityHome(), str(R.string.quality_home_title)); break;
|
||||||
case 1: pickQuality("qAway", app.qualityAway(), str(R.string.quality_away_title)); break;
|
case 1: pickQuality("qAway", app.qualityAway(), str(R.string.quality_away_title)); break;
|
||||||
case 2: pickDownloadQuality(); break;
|
case 2: pickQuality("qBt", app.qualityBluetooth(), str(R.string.quality_bt_title)); break;
|
||||||
case 3: confirmDeleteDownloads(); break;
|
case 3: pickDownloadQuality(); break;
|
||||||
case 4:
|
case 4: confirmDeleteDownloads(); break;
|
||||||
|
case 5:
|
||||||
app.prefs.edit().putBoolean("downloadFirst", !app.downloadFirst()).commit();
|
app.prefs.edit().putBoolean("downloadFirst", !app.downloadFirst()).commit();
|
||||||
toast(str(R.string.download_first_toast, str(app.downloadFirst() ? R.string.on : R.string.off)));
|
toast(str(R.string.download_first_toast, str(app.downloadFirst() ? R.string.on : R.string.off)));
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 6:
|
||||||
Server.preferSecureAtHome = !Server.preferSecureAtHome;
|
Server.preferSecureAtHome = !Server.preferSecureAtHome;
|
||||||
app.prefs.edit().putBoolean("secureHome", Server.preferSecureAtHome).commit();
|
app.prefs.edit().putBoolean("secureHome", Server.preferSecureAtHome).commit();
|
||||||
if (app.server != null) app.server.invalidate();
|
if (app.server != null) app.server.invalidate();
|
||||||
toast(str(Server.preferSecureAtHome ? R.string.https_home_on : R.string.https_home_off));
|
toast(str(Server.preferSecureAtHome ? R.string.https_home_on : R.string.https_home_off));
|
||||||
break;
|
break;
|
||||||
case 6: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_LIBRARY)); break;
|
case 7: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_LIBRARY)); break;
|
||||||
case 7: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_SERVER)); break;
|
case 8: startActivity(new Intent(BaseActivity.this, SetupActivity.class).putExtra(SetupActivity.EXTRA_MODE, SetupActivity.MODE_SERVER)); break;
|
||||||
case 8:
|
case 9:
|
||||||
if (app.server != null) {
|
if (app.server != null) {
|
||||||
app.server.invalidate();
|
app.server.invalidate();
|
||||||
toast(str(R.string.reconnecting));
|
toast(str(R.string.reconnecting));
|
||||||
@@ -480,10 +482,10 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 9: showDiagnostics(); break;
|
case 10: showDiagnostics(); break;
|
||||||
case 10: pickLanguage(); break;
|
case 11: pickLanguage(); break;
|
||||||
case 11: pickSize(); break;
|
case 12: pickSize(); break;
|
||||||
case 12: confirmSignOut(); break;
|
case 13: confirmSignOut(); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).show();
|
}).show();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
|
|||||||
if (ev == null) return;
|
if (ev == null) return;
|
||||||
if (isOrderedBroadcast()) abortBroadcast();
|
if (isOrderedBroadcast()) abortBroadcast();
|
||||||
if (ev.getAction() != KeyEvent.ACTION_DOWN || ev.getRepeatCount() > 0) return;
|
if (ev.getAction() != KeyEvent.ACTION_DOWN || ev.getRepeatCount() > 0) return;
|
||||||
|
com.minamp.net.DiagLog.log("media button " + ev.getKeyCode() + " (headset / Bluetooth)");
|
||||||
final Context app = context.getApplicationContext();
|
final Context app = context.getApplicationContext();
|
||||||
switch (ev.getKeyCode()) {
|
switch (ev.getKeyCode()) {
|
||||||
case KeyEvent.KEYCODE_HEADSETHOOK:
|
case KeyEvent.KEYCODE_HEADSETHOOK:
|
||||||
|
|||||||
@@ -144,6 +144,10 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
|||||||
private boolean hasFocus;
|
private boolean hasFocus;
|
||||||
private boolean pausedByFocus;
|
private boolean pausedByFocus;
|
||||||
private boolean pausedByCall;
|
private boolean pausedByCall;
|
||||||
|
private boolean pausedByRoute; // the Bluetooth speaker dropped: resume if it's back soon
|
||||||
|
private long routeLostAt;
|
||||||
|
private boolean btOn;
|
||||||
|
private long failedAtPos = -1; // where the last failure/stall was recovered from // audio was going to Bluetooth (A2DP), as last seen while playing
|
||||||
private WifiManager.WifiLock wifiLock;
|
private WifiManager.WifiLock wifiLock;
|
||||||
private boolean noisyRegistered;
|
private boolean noisyRegistered;
|
||||||
private boolean foreground;
|
private boolean foreground;
|
||||||
@@ -486,6 +490,8 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void trackFailed(String msg) {
|
private void trackFailed(String msg) {
|
||||||
|
failedAtPos = Math.max(currentPosition(), lastPos);
|
||||||
|
com.minamp.net.DiagLog.log("track failed: " + msg + (refusal != null ? " / " + refusal : "") + " (retry " + (retries + 1) + ")");
|
||||||
long pos = Math.max(currentPosition(), lastPos);
|
long pos = Math.max(currentPosition(), lastPos);
|
||||||
if (refusal != null) msg = refusal; // the server's own reason beats "Playback error (1/-1004)"
|
if (refusal != null) msg = refusal; // the server's own reason beats "Playback error (1/-1004)"
|
||||||
boolean permanent = refusalCode >= 400 && refusalCode < 500 && refusalCode != 408 && refusalCode != 429;
|
boolean permanent = refusalCode >= 400 && refusalCode < 500 && refusalCode != 408 && refusalCode != 429;
|
||||||
@@ -536,6 +542,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
|||||||
|
|
||||||
private void pause() {
|
private void pause() {
|
||||||
pausedByFocus = false;
|
pausedByFocus = false;
|
||||||
|
pausedByRoute = false;
|
||||||
playWhenReady = false;
|
playWhenReady = false;
|
||||||
if (mp != null && prepared) {
|
if (mp != null && prepared) {
|
||||||
try { mp.pause(); } catch (Exception ignored) {}
|
try { mp.pause(); } catch (Exception ignored) {}
|
||||||
@@ -611,6 +618,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
|||||||
|
|
||||||
private final Runnable ticker = new Runnable() {
|
private final Runnable ticker = new Runnable() {
|
||||||
public void run() {
|
public void run() {
|
||||||
|
if (state == PLAYING) btOn = audio.isBluetoothA2dpOn();
|
||||||
App.get().handler.postDelayed(this, 1000);
|
App.get().handler.postDelayed(this, 1000);
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
if (sleepAt > 0 && now >= sleepAt) {
|
if (sleepAt > 0 && now >= sleepAt) {
|
||||||
@@ -621,15 +629,19 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
|||||||
if (state == PLAYING && track != null) {
|
if (state == PLAYING && track != null) {
|
||||||
long pos = currentPosition();
|
long pos = currentPosition();
|
||||||
lastPos = pos;
|
lastPos = pos;
|
||||||
// "playing" but not moving for 12 s: say so instead of staying silent
|
// "playing" but not moving: the stream starved (Wi-Fi stalled, often because Bluetooth audio
|
||||||
|
// shares the radio) and the media player waits forever. Recover like a network error:
|
||||||
|
// reconnect and resume here; then download the track first; then move on.
|
||||||
if (pos != stallPos) {
|
if (pos != stallPos) {
|
||||||
stallPos = pos;
|
stallPos = pos;
|
||||||
stallSince = now;
|
stallSince = now;
|
||||||
stallWarned = false;
|
stallWarned = false;
|
||||||
} else if (!stallWarned && now - stallSince > 12000) {
|
// playing smoothly again for a while: a later stall starts the recovery steps afresh
|
||||||
|
if (retries > 0 && failedAtPos >= 0 && pos - failedAtPos > 30000) { retries = 0; failedAtPos = -1; }
|
||||||
|
} else if (!stallWarned && now - stallSince > 10000) {
|
||||||
stallWarned = true;
|
stallWarned = true;
|
||||||
com.minamp.net.DiagLog.log("playback stalled at " + pos / 1000 + "s");
|
com.minamp.net.DiagLog.log("playback stalled at " + pos / 1000 + "s" + (btOn ? " (Bluetooth)" : "") + ": recovering");
|
||||||
toast(getString(R.string.stalled));
|
trackFailed(getString(R.string.stalled));
|
||||||
}
|
}
|
||||||
if (!scrobbled && track.duration > 0 && pos > track.duration * 9 / 10) scrobble(track);
|
if (!scrobbled && track.duration > 0 && pos > track.duration * 9 / 10) scrobble(track);
|
||||||
if (now - lastTimeline > 10000) report(false);
|
if (now - lastTimeline > 10000) report(false);
|
||||||
@@ -692,10 +704,12 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
|||||||
if (pausedByFocus) { pausedByFocus = false; play(); }
|
if (pausedByFocus) { pausedByFocus = false; play(); }
|
||||||
break;
|
break;
|
||||||
case AudioManager.AUDIOFOCUS_LOSS:
|
case AudioManager.AUDIOFOCUS_LOSS:
|
||||||
|
com.minamp.net.DiagLog.log("paused: another app took the audio (focus lost)");
|
||||||
hasFocus = false;
|
hasFocus = false;
|
||||||
pause();
|
pause();
|
||||||
break;
|
break;
|
||||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
||||||
|
com.minamp.net.DiagLog.log("paused for a moment: another app needs the audio");
|
||||||
if (state == PLAYING) { pause(); pausedByFocus = true; }
|
if (state == PLAYING) { pause(); pausedByFocus = true; }
|
||||||
break;
|
break;
|
||||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
||||||
@@ -716,8 +730,40 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Headphones unplugged, or the Bluetooth speaker went away. Android 2.x sends this for any A2DP dropout too
|
||||||
|
* (Wi-Fi and Bluetooth share one radio on these phones, and cheap speakers drop the link now and then):
|
||||||
|
* after a Bluetooth loss, keep watching for the speaker and resume if it's back within 20 s.
|
||||||
|
*/
|
||||||
private final BroadcastReceiver noisy = new BroadcastReceiver() {
|
private final BroadcastReceiver noisy = new BroadcastReceiver() {
|
||||||
@Override public void onReceive(Context c, Intent i) { pause(); }
|
@Override public void onReceive(Context c, Intent i) {
|
||||||
|
boolean bt = btOn;
|
||||||
|
com.minamp.net.DiagLog.log("paused: audio output went away (" + (bt ? "Bluetooth speaker dropped" : "headphones unplugged") + ")");
|
||||||
|
pause();
|
||||||
|
if (bt && track != null) {
|
||||||
|
pausedByRoute = true;
|
||||||
|
routeLostAt = System.currentTimeMillis();
|
||||||
|
App.get().handler.removeCallbacks(routeWatch);
|
||||||
|
App.get().handler.postDelayed(routeWatch, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final Runnable routeWatch = new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
if (!pausedByRoute) return;
|
||||||
|
long gone = System.currentTimeMillis() - routeLostAt;
|
||||||
|
if (audio.isBluetoothA2dpOn()) {
|
||||||
|
com.minamp.net.DiagLog.log("Bluetooth speaker back after " + gone / 1000 + "s: resuming");
|
||||||
|
pausedByRoute = false;
|
||||||
|
play();
|
||||||
|
} else if (gone < 20000) {
|
||||||
|
App.get().handler.postDelayed(this, 1000);
|
||||||
|
} else {
|
||||||
|
com.minamp.net.DiagLog.log("Bluetooth speaker didn't come back: staying paused");
|
||||||
|
pausedByRoute = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private void registerNoisy() {
|
private void registerNoisy() {
|
||||||
|
|||||||
Reference in new issue
Block a user