diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 8e4d9ba6..220bfbda 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -10,7 +10,7 @@ android { applicationId "org.joinmastodon.android" minSdk 23 targetSdk 31 - versionCode 3 + versionCode 4 versionName "0.1" } diff --git a/mastodon/src/main/AndroidManifest.xml b/mastodon/src/main/AndroidManifest.xml index d6ef7361..686077fd 100644 --- a/mastodon/src/main/AndroidManifest.xml +++ b/mastodon/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="org.joinmastodon.android"> + + + \ No newline at end of file diff --git a/mastodon/src/main/java/org/joinmastodon/android/AudioPlayerService.java b/mastodon/src/main/java/org/joinmastodon/android/AudioPlayerService.java new file mode 100644 index 00000000..60ca2f80 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/AudioPlayerService.java @@ -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 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); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index e8f8fd6a..3827d10a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -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; diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Attachment.java b/mastodon/src/main/java/org/joinmastodon/android/model/Attachment.java index ceccfc3a..9dff2c4d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Attachment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Attachment.java @@ -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+ '}'; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AudioStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AudioStatusDisplayItem.java new file mode 100644 index 00000000..cb039cb6 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AudioStatusDisplayItem.java @@ -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 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)); + } + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index 2ba9805b..25a8ac5c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -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); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/SeekBarThumbDrawable.java b/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/SeekBarThumbDrawable.java new file mode 100644 index 00000000..9a7c0353 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/SeekBarThumbDrawable.java @@ -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); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java index 541cc66b..21a324c9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java @@ -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()); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index 0173d089..f9c26425 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -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)); } diff --git a/mastodon/src/main/res/drawable/ic_fluent_pause_circle_24_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_pause_circle_24_filled.xml new file mode 100644 index 00000000..df17cb79 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_pause_circle_24_filled.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_play_circle_24_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_play_circle_24_filled.xml new file mode 100644 index 00000000..237a4d54 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_play_circle_24_filled.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_ntf_logo.xml b/mastodon/src/main/res/drawable/ic_ntf_logo.xml new file mode 100644 index 00000000..eeefbbd1 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_ntf_logo.xml @@ -0,0 +1,10 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_pause_24.xml b/mastodon/src/main/res/drawable/ic_pause_24.xml new file mode 100644 index 00000000..9c312c98 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_pause_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_play_24.xml b/mastodon/src/main/res/drawable/ic_play_24.xml new file mode 100644 index 00000000..8986c1d0 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_play_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/seekbar_progress.xml b/mastodon/src/main/res/drawable/seekbar_progress.xml new file mode 100644 index 00000000..1f5f64f0 --- /dev/null +++ b/mastodon/src/main/res/drawable/seekbar_progress.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_audio.xml b/mastodon/src/main/res/layout/display_item_audio.xml new file mode 100644 index 00000000..11e370b9 --- /dev/null +++ b/mastodon/src/main/res/layout/display_item_audio.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values/attrs.xml b/mastodon/src/main/res/values/attrs.xml index 31fba285..9c490673 100644 --- a/mastodon/src/main/res/values/attrs.xml +++ b/mastodon/src/main/res/values/attrs.xml @@ -9,6 +9,8 @@ + + diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index d8670fe2..05842ff7 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -128,4 +128,7 @@ Delete Post Are you sure you want to delete this post? Deleting… + Audio playback + Play + Pause \ No newline at end of file diff --git a/mastodon/src/main/res/values/styles.xml b/mastodon/src/main/res/values/styles.xml index f5965ec9..e762dc77 100644 --- a/mastodon/src/main/res/values/styles.xml +++ b/mastodon/src/main/res/values/styles.xml @@ -27,6 +27,9 @@ @style/Theme.Mastodon.Dialog.Alert @color/primary_500 @color/gray_300 + @color/primary_600 + + @drawable/bg_button_primary_dark_on_light true true @@ -59,6 +62,9 @@ @style/Theme.Mastodon.Dialog.Alert.Dark @color/primary_700 @color/gray_600 + @color/primary_600 + + @drawable/bg_button_primary_light_on_dark false false