Audio player
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
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.ArrayList;
|
||||
import java.util.HashSet;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
|
||||
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 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));
|
||||
if(d instanceof BitmapDrawable){
|
||||
statusAvatar=((BitmapDrawable) d).getBitmap();
|
||||
}else{
|
||||
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);
|
||||
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){
|
||||
playerReady=true;
|
||||
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 void onAudioFocusChanged(int change){
|
||||
switch(change){
|
||||
case AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
resumeAfterAudioFocusGain=false;
|
||||
pause(false);
|
||||
}
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
resumeAfterAudioFocusGain=true;
|
||||
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(player.isPlaying() ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED, 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, player.isPlaying(), 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 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, boolean playing, int position);
|
||||
void onPlaybackStopped(String attachmentID);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.TileGridLayoutManager;
|
||||
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
|
||||
|
||||
@@ -67,6 +67,16 @@ public class Attachment extends BaseModel{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public double getDuration(){
|
||||
if(meta==null)
|
||||
return 0;
|
||||
if(meta.duration>0)
|
||||
return meta.duration;
|
||||
if(meta.original!=null && meta.original.duration>0)
|
||||
return meta.original.duration;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
@@ -137,6 +147,8 @@ public class Attachment extends BaseModel{
|
||||
public int width;
|
||||
public int height;
|
||||
public double aspect;
|
||||
public double duration;
|
||||
public int bitrate;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
@@ -144,6 +156,8 @@ public class Attachment extends BaseModel{
|
||||
"width="+width+
|
||||
", height="+height+
|
||||
", aspect="+aspect+
|
||||
", duration="+duration+
|
||||
", bitrate="+bitrate+
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.SystemClock;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.AudioPlayerService;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.drawables.SeekBarThumbDrawable;
|
||||
|
||||
public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
public final Attachment attachment;
|
||||
|
||||
public AudioStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status, Attachment attachment){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
this.attachment=attachment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.AUDIO;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<AudioStatusDisplayItem> implements AudioPlayerService.Callback{
|
||||
private final ImageButton playPauseBtn;
|
||||
private final TextView time;
|
||||
private final SeekBar seekBar;
|
||||
|
||||
private int lastKnownPosition;
|
||||
private long lastKnownPositionTime;
|
||||
private boolean playing;
|
||||
private int lastRemainingSeconds=-1;
|
||||
private boolean seekbarBeingDragged;
|
||||
|
||||
private Runnable positionUpdater=this::updatePosition;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_audio, parent);
|
||||
playPauseBtn=findViewById(R.id.play_pause_btn);
|
||||
time=findViewById(R.id.time);
|
||||
seekBar=findViewById(R.id.seekbar);
|
||||
seekBar.setThumb(new SeekBarThumbDrawable(context));
|
||||
playPauseBtn.setOnClickListener(this::onPlayPauseClick);
|
||||
itemView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener(){
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v){
|
||||
AudioPlayerService.registerCallback(Holder.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v){
|
||||
AudioPlayerService.unregisterCallback(Holder.this);
|
||||
}
|
||||
});
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener(){
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser){
|
||||
if(fromUser){
|
||||
int seconds=(int)(seekBar.getProgress()/10000.0*item.attachment.getDuration());
|
||||
time.setText(formatDuration(seconds));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar){
|
||||
seekbarBeingDragged=true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar){
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
service.seekTo((int)(seekBar.getProgress()/10000.0*item.attachment.getDuration()*1000.0));
|
||||
}
|
||||
seekbarBeingDragged=false;
|
||||
if(playing)
|
||||
itemView.postOnAnimation(positionUpdater);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AudioStatusDisplayItem item){
|
||||
int seconds=(int)item.attachment.getDuration();
|
||||
String duration=formatDuration(seconds);
|
||||
time.getLayoutParams().width=(int)Math.ceil(time.getPaint().measureText("-"+duration));
|
||||
time.setText(duration);
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
seekBar.setEnabled(true);
|
||||
onPlayStateChanged(item.attachment.id, service.isPlaying(), service.getPosition());
|
||||
}else{
|
||||
seekBar.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void onPlayPauseClick(View v){
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
if(playing)
|
||||
service.pause(true);
|
||||
else
|
||||
service.play();
|
||||
}else{
|
||||
AudioPlayerService.start(v.getContext(), item.status, item.attachment);
|
||||
onPlayStateChanged(item.attachment.id, true, 0);
|
||||
seekBar.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayStateChanged(String attachmentID, boolean playing, int position){
|
||||
if(attachmentID.equals(item.attachment.id)){
|
||||
this.lastKnownPosition=position;
|
||||
lastKnownPositionTime=SystemClock.uptimeMillis();
|
||||
this.playing=playing;
|
||||
playPauseBtn.setImageResource(playing ? R.drawable.ic_fluent_pause_circle_24_filled : R.drawable.ic_fluent_play_circle_24_filled);
|
||||
if(!playing){
|
||||
lastRemainingSeconds=-1;
|
||||
time.setText(formatDuration((int) item.attachment.getDuration()));
|
||||
}else{
|
||||
itemView.postOnAnimation(positionUpdater);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStopped(String attachmentID){
|
||||
if(attachmentID.equals(item.attachment.id)){
|
||||
playing=false;
|
||||
playPauseBtn.setImageResource(R.drawable.ic_fluent_play_circle_24_filled);
|
||||
seekBar.setProgress(0);
|
||||
seekBar.setEnabled(false);
|
||||
time.setText(formatDuration((int)item.attachment.getDuration()));
|
||||
}
|
||||
}
|
||||
|
||||
private String formatDuration(int seconds){
|
||||
if(seconds>=3600)
|
||||
return String.format("%d:%02d:%02d", seconds/3600, seconds%3600/60, seconds%60);
|
||||
else
|
||||
return String.format("%d:%02d", seconds/60, seconds%60);
|
||||
}
|
||||
|
||||
private void updatePosition(){
|
||||
if(!playing || seekbarBeingDragged)
|
||||
return;
|
||||
double pos=lastKnownPosition/1000.0+(SystemClock.uptimeMillis()-lastKnownPositionTime)/1000.0;
|
||||
seekBar.setProgress((int)Math.round(pos/item.attachment.getDuration()*10000.0));
|
||||
itemView.postOnAnimation(positionUpdater);
|
||||
int remainingSeconds=(int)(item.attachment.getDuration()-pos);
|
||||
if(remainingSeconds!=lastRemainingSeconds){
|
||||
lastRemainingSeconds=remainingSeconds;
|
||||
time.setText("-"+formatDuration(remainingSeconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,12 +52,12 @@ public abstract class StatusDisplayItem{
|
||||
case TEXT -> new TextStatusDisplayItem.Holder(activity, parent);
|
||||
case PHOTO -> new PhotoStatusDisplayItem.Holder(activity, parent);
|
||||
case GIFV -> new GifVStatusDisplayItem.Holder(activity, parent);
|
||||
case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent);
|
||||
case VIDEO -> new VideoStatusDisplayItem.Holder(activity, parent);
|
||||
case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent);
|
||||
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
|
||||
case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent);
|
||||
case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent);
|
||||
default -> throw new UnsupportedOperationException();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -94,6 +94,11 @@ public abstract class StatusDisplayItem{
|
||||
photoIndex++;
|
||||
}
|
||||
}
|
||||
for(Attachment att:statusForContent.mediaAttachments){
|
||||
if(att.type==Attachment.Type.AUDIO){
|
||||
items.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
|
||||
}
|
||||
}
|
||||
if(statusForContent.poll!=null){
|
||||
buildPollItems(parentID, fragment, statusForContent.poll, items);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SeekBarThumbDrawable extends Drawable{
|
||||
private Bitmap shadow1, shadow2;
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private Context context;
|
||||
|
||||
public SeekBarThumbDrawable(Context context){
|
||||
this.context=context;
|
||||
shadow1=Bitmap.createBitmap(V.dp(24), V.dp(24), Bitmap.Config.ALPHA_8);
|
||||
shadow2=Bitmap.createBitmap(V.dp(24), V.dp(24), Bitmap.Config.ALPHA_8);
|
||||
Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
paint.setColor(0xFF000000);
|
||||
paint.setShadowLayer(V.dp(2), 0, V.dp(1), 0xFF000000);
|
||||
new Canvas(shadow1).drawCircle(V.dp(12), V.dp(12), V.dp(9), paint);
|
||||
paint.setShadowLayer(V.dp(3), 0, V.dp(1), 0xFF000000);
|
||||
new Canvas(shadow2).drawCircle(V.dp(12), V.dp(12), V.dp(9), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
float centerX=getBounds().centerX();
|
||||
float centerY=getBounds().centerY();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(0x4d000000);
|
||||
canvas.drawBitmap(shadow1, centerX-shadow1.getWidth()/2f, centerY-shadow1.getHeight()/2f, paint);
|
||||
paint.setColor(0x26000000);
|
||||
canvas.drawBitmap(shadow2, centerX-shadow2.getWidth()/2f, centerY-shadow2.getHeight()/2f, paint);
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorButtonText));
|
||||
canvas.drawCircle(centerX, centerY, V.dp(7), paint);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorAccentLight));
|
||||
paint.setStrokeWidth(V.dp(4));
|
||||
canvas.drawCircle(centerX, centerY, V.dp(7), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth(){
|
||||
return V.dp(24);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight(){
|
||||
return V.dp(24);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.nodes.TextNode;
|
||||
import org.jsoup.safety.Cleaner;
|
||||
import org.jsoup.safety.Safelist;
|
||||
import org.jsoup.select.NodeVisitor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -144,4 +146,8 @@ public class HtmlParser{
|
||||
view.setText(parseCustomEmoji(text, emojis));
|
||||
UiUtils.loadCustomEmojiInTextView(view);
|
||||
}
|
||||
|
||||
public static String strip(String html){
|
||||
return Jsoup.clean(html, Safelist.none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@@ -13,7 +12,6 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.Spanned;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -40,7 +38,6 @@ import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import me.grishka.appkit.Nav;
|
||||
@@ -58,6 +55,7 @@ public class UiUtils{
|
||||
public static void launchWebBrowser(Context context, String url){
|
||||
// TODO setting for custom tabs
|
||||
new CustomTabsIntent.Builder()
|
||||
.setShowTitle(true)
|
||||
.build()
|
||||
.launchUrl(context, Uri.parse(url));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user