Audio player

This commit is contained in:
Grishka
2022-02-22 14:01:48 +03:00
parent 658415fd89
commit 4e59930d15
20 changed files with 728 additions and 5 deletions

View File

@@ -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));
}
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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));
}