moshidon/mastodon/src/main/java/org/joinmastodon/android/AudioPlayerService.java
sk22 7677ad39ca
Merge upstream redesign (#714)
* 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
2023-07-16 18:01:42 +02:00

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
}
}