
* merge toolbar fragment * Fix store screenshot generator * Fix alert color * Fix #609 * Fix crash * bigger hitbox for chips * support mastodon languages * merge ui utils * merge stuff * fix icon * ensure 48dp touch target * init local prefs, add helper function for enum values * update compose action layout * merge compose-adj files * update extended footer * fix poll wrong option checked closes sk22#641 * no border when disabled closes sk22#640 * Fix #610 * Minor fixes * Fix alert color * Fix #609 * Fix crash * Fix #610 * Minor fixes * add resources * more compatible mastodon language * fix html parser * mark as read on refresh * update tab bar * tweak m3 buttons * update compose-adj files * tweak and update styles * m3 expand button * flag icon should be 18dp, actually * More minor fixes closes #612 * More minor fixes closes #612 * Bump version * fix no create status event when redrafting * add material 3 assets * New translations strings.xml (Greek) * New translations strings.xml (Greek) * New translations strings.xml (Italian) * New translations strings.xml (Greek) * New translations strings.xml (Italian) * New translations strings.xml (Thai) * New translations strings.xml (Thai) * New translations strings.xml (Italian) * New translations strings.xml (Thai) * use new buttons for profile fragment * merge compose fragment * merge all the styles! oh dear * New translations full_description.txt (Indonesian) * New translations full_description.txt (Chinese Simplified) * New translations strings.xml (Chinese Simplified) * New translations full_description.txt (Chinese Simplified) * Fix #615 * Minor fixes * Fix #611 * A bunch of crash fixes * New translations strings.xml (Greek) * Make the default server configurable * Pass the system timezone to server when signing up * New translations strings.xml (Chinese Simplified) * New translations strings.xml (Japanese) * Fix #615 * Minor fixes * Fix #611 * A bunch of crash fixes * Make the default server configurable * Pass the system timezone to server when signing up * oops. accidentally pasted the commit message in the code * Remove unused code that caused a crash for some users ¯\_(ツ)_/¯ * New translations strings.xml (Japanese) * New translations strings.xml (Japanese) * Remove unused code that caused a crash for some users ¯\_(ツ)_/¯ * New translations strings.xml (Polish) * New translations strings.xml (Polish) * New translations strings.xml (Turkish) * New translations strings.xml (Belarusian) * prepare merging profile fragment * merge profile fragment * New translations strings.xml (Belarusian) * New translations strings.xml (Greek) * fix icon padding * apply post header changes * minor margin tweaks * fix footer buttons * fix header announcement buttons * New translations strings.xml (Japanese) * New translations strings.xml (Japanese) * New translations strings.xml (Japanese) * New translations strings.xml (Japanese) * New translations strings.xml (Japanese) * New translations strings.xml (Japanese) * New translations full_description.txt (Japanese) * New translations strings.xml (Icelandic) * New translations strings.xml (Icelandic) * New translations strings.xml (Icelandic) * fix replying * New translations strings.xml (Icelandic) * fix translate button * fix more button visibility * fix counts label styling * fix disabled boost button opacity * fix tab layouts * fix notification icon color crash * New translations strings.xml (Greek) * implement elevation listener in home tab * fix elevation and listener in home tab * add elevation scroll listener to notifications * New translations strings.xml (Scottish Gaelic) * Add editorconfig So that PRs like #625 don't happen again * Crash fix * 🤔 * New translations strings.xml (Greek) * New translations strings.xml (Japanese) * New translations strings.xml (French) * New translations strings.xml (French) * New translations strings.xml (French) * fix notification elevation and integrate divider * 🤔 * Crash fix * Add editorconfig So that PRs like #625 don't happen again * New translations strings.xml (Turkish) * save interactions in cache * New translations strings.xml (Turkish) * merge new discover/search * New translations strings.xml (Bengali) * New translations strings.xml (Scottish Gaelic) * New translations strings.xml (Bengali) * merge new settings fragments * fix no auth callback always being executed * allow opening server info from profile closes sk22#593 * fix hide boosts icon color closes sk22#676 * New translations strings.xml (Turkish) * New translations strings.xml (Turkish) * New translations strings.xml (Turkish) * New translations strings.xml (Chinese Simplified) * New translations strings.xml (Turkish) * New translations strings.xml (Chinese Simplified) * New translations strings.xml (German) * New translations strings.xml (German) * New translations strings.xml (Turkish) * update fedinuke list from source; doesn't contain any modifications regarding a recent issue * New translations strings.xml (Turkish) * remove unused class * fix crash * darken m3 outline color a bit * use m3 outline again * fix misalignment closes sk22#682 * New translations strings.xml (Turkish) * New translations full_description.txt (Turkish) * New translations short_description.txt (Turkish) * fix crash * fix metadata sorting * show pronouns in header/account lists * fix broken divider line closes sk22#679 * trim pronouns * improve pronoun display * New translations strings.xml (French) * New translations strings.xml (Japanese) * fix broken federated timeline closes sk22#685 * fix broken -1 fallback behavior closes sk22#681 * don't display nothing if server about request fails closes sk22#678 * New translations strings.xml (Ukrainian) * migrate global prefs to local prefs * do confirm unfollow by default * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * New translations full_description.txt (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Russian) * New translations strings.xml (Vietnamese) * New translations strings.xml (Ukrainian) * New translations strings.xml (Vietnamese) * New translations full_description.txt (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Vietnamese) * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * make sure list in prefs are always mutable and nut null * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Russian) * fix pronouns edge case * add back fix for stretched images closes sk22#636 * fix null pointer on missing default posting language * fix default posting language not being applied * bigger username hitbox closes sk22#688 * fix rtl header username alignment closes sk22#689 * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * hopefully fix crashes closes sk22#692 * New translations strings.xml (Ukrainian) * New translations full_description.txt (Ukrainian) * fix pronoun crash * New translations strings.xml (Persian) * New translations strings.xml (Ukrainian) * re-add true black mode * asterisk can be a pronoun * New translations strings.xml (Persian) * true black mode fixes and clean-ups * material 3 button background for switcher * darker tab bar selected background * better align follow/following button widths * restore rainbow refresh colors * fix search transition * fix min width issue with switcher button * fix no elevation when true black is enabled in light theme * use statusForContent to determine spoilerRevealed closes sk22#694 * New translations strings.xml (Persian) * New translations strings.xml (Persian) * New translations strings.xml (Persian) * New translations strings.xml (Persian) * New translations strings.xml (Persian) * New translations strings.xml (Persian) * fix profile tab bar in true black theme * fix m3 default button style closes sk22#697 * prettier role badges closes sk22#663 * fix translate button spacing closes sk22#655 * use m3 switches in dialogs closes sk22#653 * implement color palette switcher * fix color palettes being overwritten * add display and notification settings * clean up code * per-account single notification setting * add missing items to notification types * add prefix replies setting * add show replies/boosts and reply visibility * add load/see new posts settings * fix spectator mode missing spoiler padding * add a bunch of display settings * update fedinuke * add content type settings * add settings for local-onlu * add missing settings items * fix visibility button icon tint * hopefully fix some crashes * normalize padding above edit text * apparently, some people don't like pills closes sk22#706 * fix play button color closes sk22#705
373 lines
11 KiB
Java
373 lines
11 KiB
Java
package org.joinmastodon.android;
|
|
|
|
import android.app.Notification;
|
|
import android.app.NotificationChannel;
|
|
import android.app.NotificationManager;
|
|
import android.app.PendingIntent;
|
|
import android.app.Service;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.content.pm.ServiceInfo;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.drawable.BitmapDrawable;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.graphics.drawable.Icon;
|
|
import android.media.AudioManager;
|
|
import android.media.MediaMetadata;
|
|
import android.media.MediaPlayer;
|
|
import android.media.session.MediaSession;
|
|
import android.media.session.PlaybackState;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.os.IBinder;
|
|
import android.util.Log;
|
|
|
|
import org.joinmastodon.android.model.Attachment;
|
|
import org.joinmastodon.android.model.Status;
|
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
|
import org.parceler.Parcels;
|
|
|
|
import java.io.IOException;
|
|
import java.util.HashSet;
|
|
|
|
import androidx.annotation.Nullable;
|
|
import me.grishka.appkit.imageloader.ImageCache;
|
|
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
|
import me.grishka.appkit.utils.V;
|
|
|
|
public class AudioPlayerService extends Service{
|
|
private static final int NOTIFICATION_SERVICE=1;
|
|
private static final String TAG="AudioPlayerService";
|
|
private static final String ACTION_PLAY_PAUSE="org.joinmastodon.android.AUDIO_PLAY_PAUSE";
|
|
private static final String ACTION_STOP="org.joinmastodon.android.AUDIO_STOP";
|
|
|
|
private static AudioPlayerService instance;
|
|
|
|
private Status status;
|
|
private Attachment attachment;
|
|
private NotificationManager nm;
|
|
private MediaSession session;
|
|
private MediaPlayer player;
|
|
private boolean playerReady;
|
|
private Bitmap statusAvatar;
|
|
private static HashSet<Callback> callbacks=new HashSet<>();
|
|
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener=this::onAudioFocusChanged;
|
|
private boolean resumeAfterAudioFocusGain;
|
|
private boolean isBuffering=true;
|
|
|
|
private BroadcastReceiver receiver=new BroadcastReceiver(){
|
|
@Override
|
|
public void onReceive(Context context, Intent intent){
|
|
if(AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())){
|
|
pause(false);
|
|
}else if(ACTION_PLAY_PAUSE.equals(intent.getAction())){
|
|
if(!playerReady)
|
|
return;
|
|
if(player.isPlaying())
|
|
pause(false);
|
|
else
|
|
play();
|
|
}else if(ACTION_STOP.equals(intent.getAction())){
|
|
stopSelf();
|
|
}
|
|
}
|
|
};
|
|
|
|
@Nullable
|
|
@Override
|
|
public IBinder onBind(Intent intent){
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(){
|
|
super.onCreate();
|
|
nm=getSystemService(NotificationManager.class);
|
|
// registerReceiver(receiver, new IntentFilter(Intent.ACTION_MEDIA_BUTTON));
|
|
registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
|
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE));
|
|
registerReceiver(receiver, new IntentFilter(ACTION_STOP));
|
|
instance=this;
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy(){
|
|
instance=null;
|
|
unregisterReceiver(receiver);
|
|
if(player!=null){
|
|
player.release();
|
|
}
|
|
nm.cancel(NOTIFICATION_SERVICE);
|
|
for(Callback cb:callbacks)
|
|
cb.onPlaybackStopped(attachment.id);
|
|
getSystemService(AudioManager.class).abandonAudioFocus(audioFocusChangeListener);
|
|
super.onDestroy();
|
|
}
|
|
|
|
@Override
|
|
public int onStartCommand(Intent intent, int flags, int startId){
|
|
if(player!=null){
|
|
player.release();
|
|
player=null;
|
|
playerReady=false;
|
|
}
|
|
if(attachment!=null){
|
|
for(Callback cb:callbacks)
|
|
cb.onPlaybackStopped(attachment.id);
|
|
}
|
|
|
|
status=Parcels.unwrap(intent.getParcelableExtra("status"));
|
|
attachment=Parcels.unwrap(intent.getParcelableExtra("attachment"));
|
|
|
|
session=new MediaSession(this, "audioPlayer");
|
|
session.setPlaybackState(new PlaybackState.Builder()
|
|
.setState(PlaybackState.STATE_BUFFERING, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1f)
|
|
.setActions(PlaybackState.ACTION_STOP)
|
|
.build());
|
|
MediaMetadata metadata=new MediaMetadata.Builder()
|
|
.putLong(MediaMetadata.METADATA_KEY_DURATION, (long)(attachment.getDuration()*1000))
|
|
.build();
|
|
session.setMetadata(metadata);
|
|
session.setActive(true);
|
|
session.setCallback(new MediaSession.Callback(){
|
|
@Override
|
|
public void onPlay(){
|
|
play();
|
|
}
|
|
|
|
@Override
|
|
public void onPause(){
|
|
pause(false);
|
|
}
|
|
|
|
@Override
|
|
public void onStop(){
|
|
stopSelf();
|
|
}
|
|
|
|
@Override
|
|
public void onSeekTo(long pos){
|
|
seekTo((int)pos);
|
|
}
|
|
});
|
|
|
|
Drawable d=ImageCache.getInstance(this).getFromTop(new UrlImageLoaderRequest(status.account.avatar, V.dp(50), V.dp(50)));
|
|
if(d instanceof BitmapDrawable){
|
|
statusAvatar=((BitmapDrawable) d).getBitmap();
|
|
}else if(d!=null){
|
|
statusAvatar=Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
|
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
|
|
d.draw(new Canvas(statusAvatar));
|
|
}
|
|
|
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
|
|
NotificationChannel chan=new NotificationChannel("audioPlayer", getString(R.string.notification_channel_audio_player), NotificationManager.IMPORTANCE_LOW);
|
|
nm.createNotificationChannel(chan);
|
|
}
|
|
|
|
updateNotification(false, false);
|
|
getSystemService(AudioManager.class).requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
|
|
|
player=new MediaPlayer();
|
|
player.setOnPreparedListener(this::onPlayerPrepared);
|
|
player.setOnErrorListener(this::onPlayerError);
|
|
player.setOnCompletionListener(this::onPlayerCompletion);
|
|
player.setOnSeekCompleteListener(this::onPlayerSeekCompleted);
|
|
player.setOnInfoListener(this::onPlayerInfo);
|
|
try{
|
|
player.setDataSource(this, Uri.parse(attachment.url));
|
|
player.prepareAsync();
|
|
}catch(IOException x){
|
|
Log.w(TAG, "onStartCommand: error starting media player", x);
|
|
}
|
|
|
|
return START_NOT_STICKY;
|
|
}
|
|
|
|
private void onPlayerPrepared(MediaPlayer mp){
|
|
Log.i(TAG, "onPlayerPrepared");
|
|
playerReady=true;
|
|
isBuffering=false;
|
|
player.start();
|
|
updateSessionState(false);
|
|
}
|
|
|
|
private boolean onPlayerError(MediaPlayer mp, int error, int extra){
|
|
Log.e(TAG, "onPlayerError() called with: mp = ["+mp+"], error = ["+error+"], extra = ["+extra+"]");
|
|
return false;
|
|
}
|
|
|
|
private void onPlayerSeekCompleted(MediaPlayer mp){
|
|
updateSessionState(false);
|
|
}
|
|
|
|
private void onPlayerCompletion(MediaPlayer mp){
|
|
stopSelf();
|
|
}
|
|
|
|
private boolean onPlayerInfo(MediaPlayer mp, int what, int extra){
|
|
switch(what){
|
|
case MediaPlayer.MEDIA_INFO_BUFFERING_START -> {
|
|
isBuffering=true;
|
|
updateSessionState(false);
|
|
}
|
|
case MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
|
|
isBuffering=false;
|
|
updateSessionState(false);
|
|
}
|
|
default -> Log.i(TAG, "onPlayerInfo() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void onAudioFocusChanged(int change){
|
|
switch(change){
|
|
case AudioManager.AUDIOFOCUS_LOSS -> {
|
|
resumeAfterAudioFocusGain=false;
|
|
pause(false);
|
|
}
|
|
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
resumeAfterAudioFocusGain=isPlaying();
|
|
pause(false);
|
|
}
|
|
case AudioManager.AUDIOFOCUS_GAIN -> {
|
|
if(resumeAfterAudioFocusGain){
|
|
play();
|
|
}else if(isPlaying()){
|
|
player.setVolume(1f, 1f);
|
|
}
|
|
}
|
|
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
|
if(isPlaying()){
|
|
player.setVolume(.3f, .3f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void updateSessionState(boolean removeNotification){
|
|
session.setPlaybackState(new PlaybackState.Builder()
|
|
.setState(switch(getPlayState()){
|
|
case PLAYING -> PlaybackState.STATE_PLAYING;
|
|
case PAUSED -> PlaybackState.STATE_PAUSED;
|
|
case BUFFERING -> PlaybackState.STATE_BUFFERING;
|
|
}, player.getCurrentPosition(), 1f)
|
|
.setActions(PlaybackState.ACTION_STOP | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_SEEK_TO)
|
|
.build());
|
|
updateNotification(!player.isPlaying(), removeNotification);
|
|
for(Callback cb:callbacks)
|
|
cb.onPlayStateChanged(attachment.id, getPlayState(), player.getCurrentPosition());
|
|
}
|
|
|
|
private void updateNotification(boolean dismissable, boolean removeNotification){
|
|
Notification.Builder bldr=new Notification.Builder(this)
|
|
.setSmallIcon(R.drawable.ic_ntf_logo)
|
|
.setContentTitle(status.account.displayName)
|
|
.setContentText(HtmlParser.strip(status.content))
|
|
.setOngoing(!dismissable)
|
|
.setShowWhen(false)
|
|
.setDeleteIntent(PendingIntent.getBroadcast(this, 3, new Intent(ACTION_STOP), PendingIntent.FLAG_IMMUTABLE));
|
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
|
|
bldr.setChannelId("audioPlayer");
|
|
}
|
|
if(statusAvatar!=null)
|
|
bldr.setLargeIcon(statusAvatar);
|
|
|
|
Notification.MediaStyle style=new Notification.MediaStyle().setMediaSession(session.getSessionToken());
|
|
|
|
if(playerReady){
|
|
boolean isPlaying=player.isPlaying();
|
|
bldr.addAction(new Notification.Action.Builder(Icon.createWithResource(this, isPlaying ? R.drawable.ic_pause_24 : R.drawable.ic_play_24),
|
|
getString(isPlaying ? R.string.pause : R.string.play),
|
|
PendingIntent.getBroadcast(this, 2, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_IMMUTABLE))
|
|
.build());
|
|
style.setShowActionsInCompactView(0);
|
|
}
|
|
bldr.setStyle(style);
|
|
|
|
if(dismissable){
|
|
stopForeground(removeNotification);
|
|
if(!removeNotification)
|
|
nm.notify(NOTIFICATION_SERVICE, bldr.build());
|
|
}else if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.Q){
|
|
startForeground(NOTIFICATION_SERVICE, bldr.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
|
}else{
|
|
startForeground(NOTIFICATION_SERVICE, bldr.build());
|
|
}
|
|
}
|
|
|
|
public void pause(boolean removeNotification){
|
|
if(player.isPlaying()){
|
|
player.pause();
|
|
updateSessionState(removeNotification);
|
|
}
|
|
}
|
|
|
|
public void play(){
|
|
if(playerReady && !player.isPlaying()){
|
|
player.start();
|
|
updateSessionState(false);
|
|
}
|
|
}
|
|
|
|
public void seekTo(int offset){
|
|
if(playerReady){
|
|
player.seekTo(offset);
|
|
updateSessionState(false);
|
|
}
|
|
}
|
|
|
|
public boolean isPlaying(){
|
|
return playerReady && player.isPlaying();
|
|
}
|
|
|
|
public int getPosition(){
|
|
return playerReady ? player.getCurrentPosition() : 0;
|
|
}
|
|
|
|
public String getAttachmentID(){
|
|
return attachment.id;
|
|
}
|
|
|
|
public PlayState getPlayState(){
|
|
if(isBuffering)
|
|
return PlayState.BUFFERING;
|
|
return player.isPlaying() ? PlayState.PLAYING : PlayState.PAUSED;
|
|
}
|
|
|
|
public static void registerCallback(Callback cb){
|
|
callbacks.add(cb);
|
|
}
|
|
|
|
public static void unregisterCallback(Callback cb){
|
|
callbacks.remove(cb);
|
|
}
|
|
|
|
public static void start(Context context, Status status, Attachment attachment){
|
|
Intent intent=new Intent(context, AudioPlayerService.class);
|
|
intent.putExtra("status", Parcels.wrap(status));
|
|
intent.putExtra("attachment", Parcels.wrap(attachment));
|
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O)
|
|
context.startForegroundService(intent);
|
|
else
|
|
context.startService(intent);
|
|
}
|
|
|
|
public static AudioPlayerService getInstance(){
|
|
return instance;
|
|
}
|
|
|
|
public interface Callback{
|
|
void onPlayStateChanged(String attachmentID, PlayState state, int position);
|
|
void onPlaybackStopped(String attachmentID);
|
|
}
|
|
|
|
public enum PlayState{
|
|
PLAYING,
|
|
PAUSED,
|
|
BUFFERING
|
|
}
|
|
}
|