mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 09:33:33 +00:00
feat: Add home screen music playback widget and some updates in Turkish localization (#98)
This commit is contained in:
commit
f1d19142fa
24 changed files with 1862 additions and 2 deletions
|
|
@ -73,5 +73,20 @@
|
|||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.WidgetProvider4x1"
|
||||
android:exported="false"
|
||||
android:label="@string/widget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_info"/>
|
||||
</receiver>
|
||||
|
||||
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.cappielloantonio.tempo.glide;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.Log;
|
||||
|
|
@ -16,6 +17,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
|||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.signature.ObjectKey;
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.R;
|
||||
|
|
@ -109,6 +111,18 @@ public class CustomGlideRequest {
|
|||
return uri.toString();
|
||||
}
|
||||
|
||||
public static void loadAlbumArtBitmap(Context context,
|
||||
String coverId,
|
||||
int size,
|
||||
CustomTarget<Bitmap> target) {
|
||||
String url = createUrl(coverId, size);
|
||||
Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(url)
|
||||
.apply(createRequestOptions(context, coverId, ResourceType.Album))
|
||||
.into(target);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final RequestManager requestManager;
|
||||
private Object item;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package com.cappielloantonio.tempo.widget;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.session.MediaController;
|
||||
import androidx.media3.session.SessionToken;
|
||||
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public final class WidgetActions {
|
||||
public static void dispatchToMediaSession(Context ctx, String action) {
|
||||
Log.d("TempoWidget", "dispatch action=" + action);
|
||||
Context appCtx = ctx.getApplicationContext();
|
||||
SessionToken token = new SessionToken(appCtx, new ComponentName(appCtx, MediaService.class));
|
||||
ListenableFuture<MediaController> future = new MediaController.Builder(appCtx, token).buildAsync();
|
||||
future.addListener(() -> {
|
||||
try {
|
||||
if (!future.isDone()) return;
|
||||
MediaController c = future.get();
|
||||
Log.d("TempoWidget", "controller connected, isPlaying=" + c.isPlaying());
|
||||
switch (action) {
|
||||
case WidgetProvider.ACT_PLAY_PAUSE:
|
||||
if (c.isPlaying()) c.pause(); else c.play();
|
||||
break;
|
||||
case WidgetProvider.ACT_NEXT:
|
||||
c.seekToNext();
|
||||
break;
|
||||
case WidgetProvider.ACT_PREV:
|
||||
c.seekToPrevious();
|
||||
break;
|
||||
case WidgetProvider.ACT_TOGGLE_SHUFFLE:
|
||||
c.setShuffleModeEnabled(!c.getShuffleModeEnabled());
|
||||
break;
|
||||
case WidgetProvider.ACT_CYCLE_REPEAT:
|
||||
int repeatMode = c.getRepeatMode();
|
||||
int nextMode;
|
||||
if (repeatMode == Player.REPEAT_MODE_OFF) {
|
||||
nextMode = Player.REPEAT_MODE_ALL;
|
||||
} else if (repeatMode == Player.REPEAT_MODE_ALL) {
|
||||
nextMode = Player.REPEAT_MODE_ONE;
|
||||
} else {
|
||||
nextMode = Player.REPEAT_MODE_OFF;
|
||||
}
|
||||
c.setRepeatMode(nextMode);
|
||||
break;
|
||||
}
|
||||
WidgetUpdateManager.refreshFromController(ctx);
|
||||
c.release();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Log.e("TempoWidget", "dispatch failed", e);
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.cappielloantonio.tempo.widget;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.appwidget.AppWidgetProvider;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import android.app.TaskStackBuilder;
|
||||
import android.app.PendingIntent;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import android.util.Log;
|
||||
|
||||
public class WidgetProvider extends AppWidgetProvider {
|
||||
private static final String TAG = "TempoWidget";
|
||||
public static final String ACT_PLAY_PAUSE = "tempo.widget.PLAY_PAUSE";
|
||||
public static final String ACT_NEXT = "tempo.widget.NEXT";
|
||||
public static final String ACT_PREV = "tempo.widget.PREV";
|
||||
public static final String ACT_TOGGLE_SHUFFLE = "tempo.widget.SHUFFLE";
|
||||
public static final String ACT_CYCLE_REPEAT = "tempo.widget.REPEAT";
|
||||
|
||||
@Override public void onUpdate(Context ctx, AppWidgetManager mgr, int[] ids) {
|
||||
for (int id : ids) {
|
||||
RemoteViews rv = WidgetUpdateManager.chooseBuild(ctx, id);
|
||||
attachIntents(ctx, rv, id);
|
||||
mgr.updateAppWidget(id, rv);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onReceive(Context ctx, Intent intent) {
|
||||
super.onReceive(ctx, intent);
|
||||
String a = intent.getAction();
|
||||
Log.d(TAG, "onReceive action=" + a);
|
||||
if (ACT_PLAY_PAUSE.equals(a) || ACT_NEXT.equals(a) || ACT_PREV.equals(a)
|
||||
|| ACT_TOGGLE_SHUFFLE.equals(a) || ACT_CYCLE_REPEAT.equals(a)) {
|
||||
WidgetActions.dispatchToMediaSession(ctx, a);
|
||||
} else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(a)) {
|
||||
WidgetUpdateManager.refreshFromController(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, android.os.Bundle newOptions) {
|
||||
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
|
||||
RemoteViews rv = WidgetUpdateManager.chooseBuild(context, appWidgetId);
|
||||
attachIntents(context, rv, appWidgetId);
|
||||
appWidgetManager.updateAppWidget(appWidgetId, rv);
|
||||
WidgetUpdateManager.refreshFromController(context);
|
||||
}
|
||||
|
||||
public static void attachIntents(Context ctx, RemoteViews rv) {
|
||||
attachIntents(ctx, rv, 0);
|
||||
}
|
||||
|
||||
public static void attachIntents(Context ctx, RemoteViews rv, int requestCodeBase) {
|
||||
PendingIntent playPause = PendingIntent.getBroadcast(
|
||||
ctx,
|
||||
requestCodeBase + 0,
|
||||
new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_PLAY_PAUSE),
|
||||
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||
);
|
||||
PendingIntent next = PendingIntent.getBroadcast(
|
||||
ctx,
|
||||
requestCodeBase + 1,
|
||||
new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_NEXT),
|
||||
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||
);
|
||||
PendingIntent prev = PendingIntent.getBroadcast(
|
||||
ctx,
|
||||
requestCodeBase + 2,
|
||||
new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_PREV),
|
||||
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||
);
|
||||
PendingIntent shuffle = PendingIntent.getBroadcast(
|
||||
ctx,
|
||||
requestCodeBase + 3,
|
||||
new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_TOGGLE_SHUFFLE),
|
||||
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||
);
|
||||
PendingIntent repeat = PendingIntent.getBroadcast(
|
||||
ctx,
|
||||
requestCodeBase + 4,
|
||||
new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_CYCLE_REPEAT),
|
||||
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||
);
|
||||
|
||||
rv.setOnClickPendingIntent(R.id.btn_play_pause, playPause);
|
||||
rv.setOnClickPendingIntent(R.id.btn_next, next);
|
||||
rv.setOnClickPendingIntent(R.id.btn_prev, prev);
|
||||
rv.setOnClickPendingIntent(R.id.btn_shuffle, shuffle);
|
||||
rv.setOnClickPendingIntent(R.id.btn_repeat, repeat);
|
||||
|
||||
PendingIntent launch = TaskStackBuilder.create(ctx)
|
||||
.addNextIntentWithParentStack(new Intent(ctx, MainActivity.class))
|
||||
.getPendingIntent(requestCodeBase + 10, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
rv.setOnClickPendingIntent(R.id.root, launch);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.cappielloantonio.tempo.widget;
|
||||
|
||||
/**
|
||||
* AppWidget provider entry for the 4x1 widget card. Inherits all behavior
|
||||
* from {@link WidgetProvider}.
|
||||
*/
|
||||
public class WidgetProvider4x1 extends WidgetProvider {}
|
||||
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
package com.cappielloantonio.tempo.widget;
|
||||
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.text.TextUtils;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.session.MediaController;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public final class WidgetUpdateManager {
|
||||
|
||||
public static void updateFromState(Context ctx,
|
||||
String title,
|
||||
String artist,
|
||||
String album,
|
||||
Bitmap art,
|
||||
boolean playing,
|
||||
boolean shuffleEnabled,
|
||||
int repeatMode,
|
||||
long positionMs,
|
||||
long durationMs) {
|
||||
if (TextUtils.isEmpty(title)) title = ctx.getString(R.string.widget_not_playing);
|
||||
if (TextUtils.isEmpty(artist)) artist = ctx.getString(R.string.widget_placeholder_subtitle);
|
||||
if (TextUtils.isEmpty(album)) album = "";
|
||||
|
||||
final TimingInfo timing = createTimingInfo(positionMs, durationMs);
|
||||
|
||||
AppWidgetManager mgr = AppWidgetManager.getInstance(ctx);
|
||||
int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class));
|
||||
for (int id : ids) {
|
||||
android.widget.RemoteViews rv = choosePopulate(ctx, title, artist, album, art, playing,
|
||||
timing.elapsedText, timing.totalText, timing.progress, shuffleEnabled, repeatMode, id);
|
||||
WidgetProvider.attachIntents(ctx, rv, id);
|
||||
mgr.updateAppWidget(id, rv);
|
||||
}
|
||||
}
|
||||
|
||||
public static void pushNow(Context ctx) {
|
||||
AppWidgetManager mgr = AppWidgetManager.getInstance(ctx);
|
||||
int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class));
|
||||
for (int id : ids) {
|
||||
android.widget.RemoteViews rv = chooseBuild(ctx, id);
|
||||
WidgetProvider.attachIntents(ctx, rv, id);
|
||||
mgr.updateAppWidget(id, rv);
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateFromState(Context ctx,
|
||||
String title,
|
||||
String artist,
|
||||
String album,
|
||||
String coverArtId,
|
||||
boolean playing,
|
||||
boolean shuffleEnabled,
|
||||
int repeatMode,
|
||||
long positionMs,
|
||||
long durationMs) {
|
||||
final Context appCtx = ctx.getApplicationContext();
|
||||
final String t = TextUtils.isEmpty(title) ? appCtx.getString(R.string.widget_not_playing) : title;
|
||||
final String a = TextUtils.isEmpty(artist) ? appCtx.getString(R.string.widget_placeholder_subtitle) : artist;
|
||||
final String alb = !TextUtils.isEmpty(album) ? album : "";
|
||||
final boolean p = playing;
|
||||
final boolean sh = shuffleEnabled;
|
||||
final int rep = repeatMode;
|
||||
final TimingInfo timing = createTimingInfo(positionMs, durationMs);
|
||||
|
||||
if (!TextUtils.isEmpty(coverArtId)) {
|
||||
CustomGlideRequest.loadAlbumArtBitmap(
|
||||
appCtx,
|
||||
coverArtId,
|
||||
com.cappielloantonio.tempo.util.Preferences.getImageSize(),
|
||||
new CustomTarget<Bitmap>() {
|
||||
@Override public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) {
|
||||
AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx);
|
||||
int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class));
|
||||
for (int id : ids) {
|
||||
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, resource, p,
|
||||
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
|
||||
WidgetProvider.attachIntents(appCtx, rv, id);
|
||||
mgr.updateAppWidget(id, rv);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onLoadCleared(Drawable placeholder) {
|
||||
AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx);
|
||||
int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class));
|
||||
for (int id : ids) {
|
||||
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
|
||||
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
|
||||
WidgetProvider.attachIntents(appCtx, rv, id);
|
||||
mgr.updateAppWidget(id, rv);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx);
|
||||
int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class));
|
||||
for (int id : ids) {
|
||||
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
|
||||
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
|
||||
WidgetProvider.attachIntents(appCtx, rv, id);
|
||||
mgr.updateAppWidget(id, rv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void refreshFromController(Context ctx) {
|
||||
final Context appCtx = ctx.getApplicationContext();
|
||||
SessionToken token = new SessionToken(appCtx, new ComponentName(appCtx, MediaService.class));
|
||||
ListenableFuture<MediaController> future = new MediaController.Builder(appCtx, token).buildAsync();
|
||||
future.addListener(() -> {
|
||||
try {
|
||||
if (!future.isDone()) return;
|
||||
MediaController c = future.get();
|
||||
androidx.media3.common.MediaItem mi = c.getCurrentMediaItem();
|
||||
String title = null, artist = null, album = null, coverId = null;
|
||||
if (mi != null && mi.mediaMetadata != null) {
|
||||
if (mi.mediaMetadata.title != null) title = mi.mediaMetadata.title.toString();
|
||||
if (mi.mediaMetadata.artist != null) artist = mi.mediaMetadata.artist.toString();
|
||||
if (mi.mediaMetadata.albumTitle != null) album = mi.mediaMetadata.albumTitle.toString();
|
||||
if (mi.mediaMetadata.extras != null) {
|
||||
if (title == null) title = mi.mediaMetadata.extras.getString("title");
|
||||
if (artist == null) artist = mi.mediaMetadata.extras.getString("artist");
|
||||
if (album == null) album = mi.mediaMetadata.extras.getString("album");
|
||||
coverId = mi.mediaMetadata.extras.getString("coverArtId");
|
||||
}
|
||||
}
|
||||
long position = c.getCurrentPosition();
|
||||
long duration = c.getDuration();
|
||||
if (position == C.TIME_UNSET) position = 0;
|
||||
if (duration == C.TIME_UNSET) duration = 0;
|
||||
updateFromState(appCtx,
|
||||
title != null ? title : appCtx.getString(R.string.widget_not_playing),
|
||||
artist != null ? artist : appCtx.getString(R.string.widget_placeholder_subtitle),
|
||||
album,
|
||||
coverId,
|
||||
c.isPlaying(),
|
||||
c.getShuffleModeEnabled(),
|
||||
c.getRepeatMode(),
|
||||
position,
|
||||
duration);
|
||||
c.release();
|
||||
} catch (ExecutionException | InterruptedException ignored) {
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private static TimingInfo createTimingInfo(long positionMs, long durationMs) {
|
||||
long safePosition = Math.max(0L, positionMs);
|
||||
long safeDuration = durationMs > 0 ? durationMs : 0L;
|
||||
if (safeDuration > 0 && safePosition > safeDuration) {
|
||||
safePosition = safeDuration;
|
||||
}
|
||||
|
||||
String elapsed = (safeDuration > 0 || safePosition > 0)
|
||||
? MusicUtil.getReadableDurationString(safePosition, true)
|
||||
: null;
|
||||
String total = safeDuration > 0
|
||||
? MusicUtil.getReadableDurationString(safeDuration, true)
|
||||
: null;
|
||||
|
||||
int progress = 0;
|
||||
if (safeDuration > 0) {
|
||||
long scaled = safePosition * WidgetViewsFactory.PROGRESS_MAX;
|
||||
long progressLong = scaled / safeDuration;
|
||||
if (progressLong < 0) {
|
||||
progress = 0;
|
||||
} else if (progressLong > WidgetViewsFactory.PROGRESS_MAX) {
|
||||
progress = WidgetViewsFactory.PROGRESS_MAX;
|
||||
} else {
|
||||
progress = (int) progressLong;
|
||||
}
|
||||
}
|
||||
|
||||
return new TimingInfo(elapsed, total, progress);
|
||||
}
|
||||
|
||||
public static android.widget.RemoteViews chooseBuild(Context ctx, int appWidgetId) {
|
||||
LayoutSize size = resolveLayoutSize(ctx, appWidgetId);
|
||||
switch (size) {
|
||||
case MEDIUM:
|
||||
return WidgetViewsFactory.buildMedium(ctx);
|
||||
case LARGE:
|
||||
return WidgetViewsFactory.buildLarge(ctx);
|
||||
case EXPANDED:
|
||||
return WidgetViewsFactory.buildExpanded(ctx);
|
||||
case COMPACT:
|
||||
default:
|
||||
return WidgetViewsFactory.buildCompact(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private static android.widget.RemoteViews choosePopulate(Context ctx,
|
||||
String title,
|
||||
String artist,
|
||||
String album,
|
||||
Bitmap art,
|
||||
boolean playing,
|
||||
String elapsedText,
|
||||
String totalText,
|
||||
int progress,
|
||||
boolean shuffleEnabled,
|
||||
int repeatMode,
|
||||
int appWidgetId) {
|
||||
LayoutSize size = resolveLayoutSize(ctx, appWidgetId);
|
||||
switch (size) {
|
||||
case MEDIUM:
|
||||
return WidgetViewsFactory.populateMedium(ctx, title, artist, album, art, playing,
|
||||
elapsedText, totalText, progress, shuffleEnabled, repeatMode);
|
||||
case LARGE:
|
||||
return WidgetViewsFactory.populateLarge(ctx, title, artist, album, art, playing,
|
||||
elapsedText, totalText, progress, shuffleEnabled, repeatMode);
|
||||
case EXPANDED:
|
||||
return WidgetViewsFactory.populateExpanded(ctx, title, artist, album, art, playing,
|
||||
elapsedText, totalText, progress, shuffleEnabled, repeatMode);
|
||||
case COMPACT:
|
||||
default:
|
||||
return WidgetViewsFactory.populateCompact(ctx, title, artist, album, art, playing,
|
||||
elapsedText, totalText, progress, shuffleEnabled, repeatMode);
|
||||
}
|
||||
}
|
||||
|
||||
private static LayoutSize resolveLayoutSize(Context ctx, int appWidgetId) {
|
||||
AppWidgetManager mgr = AppWidgetManager.getInstance(ctx);
|
||||
android.os.Bundle opts = mgr.getAppWidgetOptions(appWidgetId);
|
||||
int minH = opts != null ? opts.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) : 0;
|
||||
int expandedThreshold = ctx.getResources().getInteger(R.integer.widget_expanded_min_height_dp);
|
||||
int largeThreshold = ctx.getResources().getInteger(R.integer.widget_large_min_height_dp);
|
||||
int mediumThreshold = ctx.getResources().getInteger(R.integer.widget_medium_min_height_dp);
|
||||
if (minH >= expandedThreshold) return LayoutSize.EXPANDED;
|
||||
if (minH >= largeThreshold) return LayoutSize.LARGE;
|
||||
if (minH >= mediumThreshold) return LayoutSize.MEDIUM;
|
||||
return LayoutSize.COMPACT;
|
||||
}
|
||||
|
||||
private enum LayoutSize {
|
||||
COMPACT,
|
||||
MEDIUM,
|
||||
LARGE,
|
||||
EXPANDED
|
||||
}
|
||||
|
||||
private static final class TimingInfo {
|
||||
final String elapsedText;
|
||||
final String totalText;
|
||||
final int progress;
|
||||
|
||||
TimingInfo(String elapsedText, String totalText, int progress) {
|
||||
this.elapsedText = elapsedText;
|
||||
this.totalText = totalText;
|
||||
this.progress = progress;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
package com.cappielloantonio.tempo.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapShader;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.media3.common.Player;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
|
||||
public final class WidgetViewsFactory {
|
||||
|
||||
static final int PROGRESS_MAX = 1000;
|
||||
private static final float ALBUM_ART_CORNER_RADIUS_DP = 6f;
|
||||
|
||||
private WidgetViewsFactory() {}
|
||||
|
||||
public static RemoteViews buildCompact(Context ctx) {
|
||||
return build(ctx, R.layout.widget_layout_compact, false, false);
|
||||
}
|
||||
|
||||
public static RemoteViews buildMedium(Context ctx) {
|
||||
return build(ctx, R.layout.widget_layout_medium, false, false);
|
||||
}
|
||||
|
||||
public static RemoteViews buildLarge(Context ctx) {
|
||||
return build(ctx, R.layout.widget_layout_large_short, true, true);
|
||||
}
|
||||
|
||||
public static RemoteViews buildExpanded(Context ctx) {
|
||||
return build(ctx, R.layout.widget_layout_large, true, true);
|
||||
}
|
||||
|
||||
private static RemoteViews build(Context ctx,
|
||||
int layoutRes,
|
||||
boolean showAlbum,
|
||||
boolean showSecondaryControls) {
|
||||
RemoteViews rv = new RemoteViews(ctx.getPackageName(), layoutRes);
|
||||
rv.setTextViewText(R.id.title, ctx.getString(R.string.widget_not_playing));
|
||||
rv.setTextViewText(R.id.subtitle, ctx.getString(R.string.widget_placeholder_subtitle));
|
||||
rv.setTextViewText(R.id.album, "");
|
||||
rv.setViewVisibility(R.id.album, showAlbum ? View.INVISIBLE : View.GONE);
|
||||
rv.setTextViewText(R.id.time_elapsed, ctx.getString(R.string.widget_time_elapsed_placeholder));
|
||||
rv.setTextViewText(R.id.time_total, ctx.getString(R.string.widget_time_duration_placeholder));
|
||||
rv.setProgressBar(R.id.progress, PROGRESS_MAX, 0, false);
|
||||
rv.setImageViewResource(R.id.btn_play_pause, R.drawable.ic_play);
|
||||
rv.setImageViewResource(R.id.album_art, R.drawable.ic_splash_logo);
|
||||
applySecondaryControlsDefaults(ctx, rv, showSecondaryControls);
|
||||
return rv;
|
||||
}
|
||||
|
||||
private static void applySecondaryControlsDefaults(Context ctx,
|
||||
RemoteViews rv,
|
||||
boolean show) {
|
||||
int visibility = show ? View.VISIBLE : View.GONE;
|
||||
rv.setViewVisibility(R.id.controls_secondary, visibility);
|
||||
rv.setViewVisibility(R.id.btn_shuffle, visibility);
|
||||
rv.setViewVisibility(R.id.btn_repeat, visibility);
|
||||
if (show) {
|
||||
int defaultColor = ContextCompat.getColor(ctx, R.color.widget_icon_tint);
|
||||
rv.setImageViewResource(R.id.btn_shuffle, R.drawable.ic_shuffle);
|
||||
rv.setImageViewResource(R.id.btn_repeat, R.drawable.ic_repeat);
|
||||
rv.setInt(R.id.btn_shuffle, "setColorFilter", defaultColor);
|
||||
rv.setInt(R.id.btn_repeat, "setColorFilter", defaultColor);
|
||||
}
|
||||
}
|
||||
|
||||
public static RemoteViews populateCompact(Context ctx,
|
||||
String title,
|
||||
String subtitle,
|
||||
String album,
|
||||
Bitmap art,
|
||||
boolean playing,
|
||||
String elapsedText,
|
||||
String totalText,
|
||||
int progress,
|
||||
boolean shuffleEnabled,
|
||||
int repeatMode) {
|
||||
return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText,
|
||||
progress, R.layout.widget_layout_compact, false, false, shuffleEnabled, repeatMode);
|
||||
}
|
||||
|
||||
public static RemoteViews populateMedium(Context ctx,
|
||||
String title,
|
||||
String subtitle,
|
||||
String album,
|
||||
Bitmap art,
|
||||
boolean playing,
|
||||
String elapsedText,
|
||||
String totalText,
|
||||
int progress,
|
||||
boolean shuffleEnabled,
|
||||
int repeatMode) {
|
||||
return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText,
|
||||
progress, R.layout.widget_layout_medium, true, true, shuffleEnabled, repeatMode);
|
||||
}
|
||||
|
||||
public static RemoteViews populateLarge(Context ctx,
|
||||
String title,
|
||||
String subtitle,
|
||||
String album,
|
||||
Bitmap art,
|
||||
boolean playing,
|
||||
String elapsedText,
|
||||
String totalText,
|
||||
int progress,
|
||||
boolean shuffleEnabled,
|
||||
int repeatMode) {
|
||||
return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText,
|
||||
progress, R.layout.widget_layout_large_short, true, true, shuffleEnabled, repeatMode);
|
||||
}
|
||||
|
||||
public static RemoteViews populateExpanded(Context ctx,
|
||||
String title,
|
||||
String subtitle,
|
||||
String album,
|
||||
Bitmap art,
|
||||
boolean playing,
|
||||
String elapsedText,
|
||||
String totalText,
|
||||
int progress,
|
||||
boolean shuffleEnabled,
|
||||
int repeatMode) {
|
||||
return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText,
|
||||
progress, R.layout.widget_layout_large, true, true, shuffleEnabled, repeatMode);
|
||||
}
|
||||
|
||||
private static RemoteViews populateWithLayout(Context ctx,
|
||||
String title,
|
||||
String subtitle,
|
||||
String album,
|
||||
Bitmap art,
|
||||
boolean playing,
|
||||
String elapsedText,
|
||||
String totalText,
|
||||
int progress,
|
||||
int layoutRes,
|
||||
boolean showAlbum,
|
||||
boolean showSecondaryControls,
|
||||
boolean shuffleEnabled,
|
||||
int repeatMode) {
|
||||
RemoteViews rv = new RemoteViews(ctx.getPackageName(), layoutRes);
|
||||
rv.setTextViewText(R.id.title, title);
|
||||
rv.setTextViewText(R.id.subtitle, subtitle);
|
||||
|
||||
if (showAlbum && !TextUtils.isEmpty(album)) {
|
||||
rv.setTextViewText(R.id.album, album);
|
||||
rv.setViewVisibility(R.id.album, View.VISIBLE);
|
||||
} else {
|
||||
rv.setTextViewText(R.id.album, "");
|
||||
rv.setViewVisibility(R.id.album, View.GONE);
|
||||
}
|
||||
|
||||
if (art != null) {
|
||||
Bitmap rounded = maybeRoundBitmap(ctx, art);
|
||||
rv.setImageViewBitmap(R.id.album_art, rounded != null ? rounded : art);
|
||||
} else {
|
||||
rv.setImageViewResource(R.id.album_art, R.drawable.ic_splash_logo);
|
||||
}
|
||||
|
||||
rv.setImageViewResource(R.id.btn_play_pause,
|
||||
playing ? R.drawable.ic_pause : R.drawable.ic_play);
|
||||
|
||||
String elapsed = !TextUtils.isEmpty(elapsedText)
|
||||
? elapsedText
|
||||
: ctx.getString(R.string.widget_time_elapsed_placeholder);
|
||||
String total = !TextUtils.isEmpty(totalText)
|
||||
? totalText
|
||||
: ctx.getString(R.string.widget_time_duration_placeholder);
|
||||
|
||||
int safeProgress = progress;
|
||||
if (safeProgress < 0) safeProgress = 0;
|
||||
if (safeProgress > PROGRESS_MAX) safeProgress = PROGRESS_MAX;
|
||||
|
||||
rv.setTextViewText(R.id.time_elapsed, elapsed);
|
||||
rv.setTextViewText(R.id.time_total, total);
|
||||
rv.setProgressBar(R.id.progress, PROGRESS_MAX, safeProgress, false);
|
||||
|
||||
applySecondaryControls(ctx, rv, showSecondaryControls, shuffleEnabled, repeatMode);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
private static Bitmap maybeRoundBitmap(Context ctx, Bitmap source) {
|
||||
if (source == null || source.isRecycled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
int width = source.getWidth();
|
||||
int height = source.getHeight();
|
||||
if (width <= 0 || height <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(output);
|
||||
|
||||
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
paint.setShader(new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
|
||||
|
||||
float radiusPx = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
ALBUM_ART_CORNER_RADIUS_DP,
|
||||
ctx.getResources().getDisplayMetrics());
|
||||
float maxRadius = Math.min(width, height) / 2f;
|
||||
float safeRadius = Math.min(radiusPx, maxRadius);
|
||||
|
||||
canvas.drawRoundRect(new RectF(0f, 0f, width, height), safeRadius, safeRadius, paint);
|
||||
return output;
|
||||
} catch (RuntimeException | OutOfMemoryError e) {
|
||||
android.util.Log.w("TempoWidget", "Failed to round album art", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void applySecondaryControls(Context ctx,
|
||||
RemoteViews rv,
|
||||
boolean show,
|
||||
boolean shuffleEnabled,
|
||||
int repeatMode) {
|
||||
if (!show) {
|
||||
rv.setViewVisibility(R.id.controls_secondary, View.GONE);
|
||||
rv.setViewVisibility(R.id.btn_shuffle, View.GONE);
|
||||
rv.setViewVisibility(R.id.btn_repeat, View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
int inactiveColor = ContextCompat.getColor(ctx, R.color.widget_icon_tint);
|
||||
int activeColor = ContextCompat.getColor(ctx, R.color.widget_icon_tint_active);
|
||||
|
||||
rv.setViewVisibility(R.id.controls_secondary, View.VISIBLE);
|
||||
rv.setViewVisibility(R.id.btn_shuffle, View.VISIBLE);
|
||||
rv.setViewVisibility(R.id.btn_repeat, View.VISIBLE);
|
||||
rv.setImageViewResource(R.id.btn_shuffle, R.drawable.ic_shuffle);
|
||||
rv.setImageViewResource(R.id.btn_repeat,
|
||||
repeatMode == Player.REPEAT_MODE_ONE ? R.drawable.ic_repeat_one : R.drawable.ic_repeat);
|
||||
rv.setInt(R.id.btn_shuffle, "setColorFilter", shuffleEnabled ? activeColor : inactiveColor);
|
||||
rv.setInt(R.id.btn_repeat, "setColorFilter",
|
||||
repeatMode == Player.REPEAT_MODE_OFF ? inactiveColor : activeColor);
|
||||
}
|
||||
}
|
||||
12
app/src/main/res/drawable/ic_repeat_one.xml
Normal file
12
app/src/main/res/drawable/ic_repeat_one.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/titleTextColor"
|
||||
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17H7v-3l-4,4 4,4v-3h12v-6h-2v4z" />
|
||||
<path
|
||||
android:fillColor="@color/titleTextColor"
|
||||
android:pathData="M12,9h-2v2h1v6h2V9h-1z" />
|
||||
</vector>
|
||||
6
app/src/main/res/drawable/widget_bg.xml
Normal file
6
app/src/main/res/drawable/widget_bg.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="10dp" />
|
||||
<solid android:color="@color/widget_bg" />
|
||||
</shape>
|
||||
175
app/src/main/res/layout/widget_layout_compact.xml
Normal file
175
app/src/main/res/layout/widget_layout_compact.xml
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:background="@drawable/widget_bg">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/album_art"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="@string/widget_content_desc_album_art"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/texts"
|
||||
android:orientation="vertical"
|
||||
android:layout_toRightOf="@id/album_art"
|
||||
android:layout_toEndOf="@id/album_art"
|
||||
android:layout_toLeftOf="@id/controls"
|
||||
android:layout_toStartOf="@id/controls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textStyle="bold"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/widget_title"
|
||||
android:includeFontPadding="false"
|
||||
android:freezesText="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:includeFontPadding="false"
|
||||
android:freezesText="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textSize="11sp"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:includeFontPadding="false"
|
||||
android:freezesText="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:indeterminate="false"
|
||||
android:max="1000"
|
||||
android:progress="0"
|
||||
android:progressBackgroundTint="@color/widget_subtitle"
|
||||
android:progressTint="@color/widget_icon_tint"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/timing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_elapsed"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/widget_time_elapsed_placeholder"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:textSize="10sp"
|
||||
android:includeFontPadding="false"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_total"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:text="@string/widget_time_duration_placeholder"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:textSize="10sp"
|
||||
android:includeFontPadding="false"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls_secondary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="4dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_shuffle"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_shuffle"
|
||||
android:src="@drawable/ic_shuffle"
|
||||
android:tint="@color/widget_icon_tint"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_repeat"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_repeat"
|
||||
android:src="@drawable/ic_repeat"
|
||||
android:tint="@color/widget_icon_tint"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_prev"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_skip_previous"
|
||||
android:contentDescription="@string/widget_content_desc_prev"
|
||||
android:tint="@color/widget_icon_tint"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_play_pause"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_play"
|
||||
android:contentDescription="@string/widget_content_desc_play_pause"
|
||||
android:tint="@color/widget_icon_tint"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_next"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_skip_next"
|
||||
android:contentDescription="@string/widget_content_desc_next"
|
||||
android:tint="@color/widget_icon_tint"/>
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
189
app/src/main/res/layout/widget_layout_large.xml
Normal file
189
app/src/main/res/layout/widget_layout_large.xml
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minHeight="200dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:background="@drawable/widget_bg">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/album_art"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="150dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="@string/widget_content_desc_album_art" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/text_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:textStyle="bold"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/widget_title"
|
||||
android:includeFontPadding="false"
|
||||
android:freezesText="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:includeFontPadding="false"
|
||||
android:freezesText="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:includeFontPadding="false"
|
||||
android:freezesText="true"
|
||||
android:visibility="invisible" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="6dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:indeterminate="false"
|
||||
android:max="1000"
|
||||
android:progress="0"
|
||||
android:progressBackgroundTint="@color/widget_subtitle"
|
||||
android:progressTint="@color/widget_icon_tint" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/timing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_elapsed"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/widget_time_elapsed_placeholder"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_total"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:text="@string/widget_time_duration_placeholder"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_prev"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_prev"
|
||||
android:src="@drawable/ic_skip_previous"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_play_pause"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_play_pause"
|
||||
android:src="@drawable/ic_play"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_next"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_next"
|
||||
android:src="@drawable/ic_skip_next"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls_secondary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_shuffle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_shuffle"
|
||||
android:src="@drawable/ic_shuffle"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_repeat"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_repeat"
|
||||
android:src="@drawable/ic_repeat"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
198
app/src/main/res/layout/widget_layout_large_short.xml
Normal file
198
app/src/main/res/layout/widget_layout_large_short.xml
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minHeight="172dp"
|
||||
android:padding="16dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/widget_bg">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:baselineAligned="false"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/album_art_container"
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="90dp"
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/album_art"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="@string/widget_content_desc_album_art" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/text_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:textStyle="bold"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/widget_title"
|
||||
android:includeFontPadding="false"
|
||||
android:freezesText="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:includeFontPadding="false"
|
||||
android:freezesText="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:includeFontPadding="false"
|
||||
android:freezesText="true"
|
||||
android:visibility="invisible" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="6dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:indeterminate="false"
|
||||
android:max="1000"
|
||||
android:progress="0"
|
||||
android:progressBackgroundTint="@color/widget_subtitle"
|
||||
android:progressTint="@color/widget_icon_tint" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/timing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_elapsed"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/widget_time_elapsed_placeholder"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_total"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:text="@string/widget_time_duration_placeholder"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls_secondary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_shuffle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_shuffle"
|
||||
android:src="@drawable/ic_shuffle"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="3"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_prev"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_prev"
|
||||
android:src="@drawable/ic_skip_previous"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_play_pause"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_play_pause"
|
||||
android:src="@drawable/ic_play"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_next"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_next"
|
||||
android:src="@drawable/ic_skip_next"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_repeat"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_repeat"
|
||||
android:src="@drawable/ic_repeat"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
216
app/src/main/res/layout/widget_layout_medium.xml
Normal file
216
app/src/main/res/layout/widget_layout_medium.xml
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minHeight="120dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:orientation="horizontal"
|
||||
android:baselineAligned="false"
|
||||
android:background="@drawable/widget_bg">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/album_art_container"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/album_art"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="@string/widget_content_desc_album_art" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:weightSum="1">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/text_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:textStyle="bold"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/widget_title"
|
||||
android:includeFontPadding="false"
|
||||
android:freezesText="true" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/subtitle_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="1dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:includeFontPadding="false"
|
||||
android:freezesText="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:gravity="end"
|
||||
android:textAlignment="viewEnd"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:includeFontPadding="false"
|
||||
android:freezesText="true"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="3dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:indeterminate="false"
|
||||
android:max="1000"
|
||||
android:progress="0"
|
||||
android:progressBackgroundTint="@color/widget_subtitle"
|
||||
android:progressTint="@color/widget_icon_tint" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/timing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_elapsed"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/widget_time_elapsed_placeholder"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:textSize="10sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time_total"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:text="@string/widget_time_duration_placeholder"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls_secondary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_shuffle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_shuffle"
|
||||
android:src="@drawable/ic_shuffle"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="3"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_prev"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="1dp"
|
||||
android:layout_marginEnd="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_prev"
|
||||
android:src="@drawable/ic_skip_previous"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_play_pause"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="34dp"
|
||||
android:layout_marginStart="1dp"
|
||||
android:layout_marginEnd="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_play_pause"
|
||||
android:src="@drawable/ic_play"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_next"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="1dp"
|
||||
android:layout_marginEnd="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_next"
|
||||
android:src="@drawable/ic_skip_next"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_repeat"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="1dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/widget_content_desc_repeat"
|
||||
android:src="@drawable/ic_repeat"
|
||||
android:tint="@color/widget_icon_tint" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
82
app/src/main/res/layout/widget_preview_compact.xml
Normal file
82
app/src/main/res/layout/widget_preview_compact.xml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingRight="0dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:background="@drawable/widget_bg">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/album_art"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_splash_logo"
|
||||
android:contentDescription="@string/widget_content_desc_album_art"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/texts"
|
||||
android:orientation="vertical"
|
||||
android:layout_toEndOf="@id/album_art"
|
||||
android:layout_toStartOf="@id/controls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textStyle="bold"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/widget_title"
|
||||
android:text="@string/widget_not_playing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/widget_subtitle"
|
||||
android:text="@string/widget_placeholder_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controls"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageButton android:id="@+id/btn_prev"
|
||||
android:layout_width="48dp" android:layout_height="48dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_skip_previous"
|
||||
android:tint="@color/widget_icon_tint"
|
||||
android:contentDescription="@string/widget_content_desc_prev"/>
|
||||
|
||||
<ImageButton android:id="@+id/btn_play_pause"
|
||||
android:layout_width="48dp" android:layout_height="48dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_play"
|
||||
android:tint="@color/widget_icon_tint"
|
||||
android:contentDescription="@string/widget_content_desc_play_pause"/>
|
||||
|
||||
<ImageButton android:id="@+id/btn_next"
|
||||
android:layout_width="48dp" android:layout_height="48dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_skip_next"
|
||||
android:tint="@color/widget_icon_tint"
|
||||
android:contentDescription="@string/widget_content_desc_next"/>
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
7
app/src/main/res/values-night/colors_widget.xml
Normal file
7
app/src/main/res/values-night/colors_widget.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="widget_bg">#CC000000</color>
|
||||
<color name="widget_title">#FFFFFFFF</color>
|
||||
<color name="widget_subtitle">#B3FFFFFF</color>
|
||||
<color name="widget_icon_tint">#FFFFFFFF</color>
|
||||
</resources>
|
||||
|
|
@ -90,6 +90,7 @@
|
|||
<string name="exo_download_notification_channel_name">İndirilenler</string>
|
||||
<string name="filter_info_selection">İki veya daha fazla filtre seçin</string>
|
||||
<string name="filter_title">Filtre</string>
|
||||
<string name="filter_artist">Sanatçıları filtrele</string>
|
||||
<string name="filter_title_expanded">Türleri filtrele</string>
|
||||
<string name="generic_list_page_count">(%1$d)</string>
|
||||
<string name="generic_list_page_count_unknown">(+%1$d)</string>
|
||||
|
|
@ -116,6 +117,7 @@
|
|||
<string name="home_sync_starred_download">İndir</string>
|
||||
<string name="home_sync_starred_subtitle">Bu parçaların indirilmesi önemli miktarda veri kullanabilir</string>
|
||||
<string name="home_sync_starred_title">Eşitlenecek bazı yıldızlı parçalar var gibi görünüyor</string>
|
||||
<string name="home_sync_starred_albums_subtitle">Yıldız ile işaretlenen albümler çevrimdışı kullanılabilir olacak</string>
|
||||
<string name="home_title_best_of">En iyiler</string>
|
||||
<string name="home_title_discovery">Keşfet</string>
|
||||
<string name="home_title_discovery_shuffle_all_button">Tümünü karıştır</string>
|
||||
|
|
@ -164,6 +166,7 @@
|
|||
<string name="menu_add_button">Ekle</string>
|
||||
<string name="menu_add_to_playlist_button">Çalma listesine ekle</string>
|
||||
<string name="menu_download_all_button">Tümünü indir</string>
|
||||
<string name="menu_rate_album">Albümü oyla</string>
|
||||
<string name="menu_download_label">İndir</string>
|
||||
<string name="menu_filter_all">Tümü</string>
|
||||
<string name="menu_filter_download">İndirilenler</string>
|
||||
|
|
@ -192,6 +195,7 @@
|
|||
<string name="menu_sort_year">Yıl</string>
|
||||
<string name="player_playback_speed">%1$.2fx</string>
|
||||
<string name="player_queue_clean_all_button">Çalma sırasını temizle</string>
|
||||
<string name="player_queue_save_queue_success">Kayıtlı oynatma sırası</string>
|
||||
<string name="player_server_priority">Sunucu önceliği</string>
|
||||
<string name="player_unknown_format">Bilinmeyen format</string>
|
||||
<string name="player_transcoding">Dönüştürme</string>
|
||||
|
|
@ -311,6 +315,7 @@
|
|||
<string name="settings_podcast_summary">Etkinleştirildiğinde podcast bölümü görüntülenir. Tam etkili olması için uygulamayı yeniden başlatın.</string>
|
||||
<string name="settings_audio_quality">Ses kalitesini göster</string>
|
||||
<string name="settings_audio_quality_summary">Her ses parçası için bit hızı ve ses formatı gösterilecektir.</string>
|
||||
<string name="settings_song_rating_summary">" "</string>
|
||||
<string name="settings_item_rating">Öğe değerlemesini göster</string>
|
||||
<string name="settings_item_rating_summary">Etkinleştirildiğinde, öğenin puanı ve favori olarak işaretlenip işaretlenmediği görüntülenir.</string>
|
||||
<string name="settings_queue_syncing_countdown">Eşitleme zamanlayıcısı</string>
|
||||
|
|
@ -340,6 +345,7 @@
|
|||
<string name="settings_summary_transcoding_download">Dönüştürülmüş medyayı indir. Etkinleştirilirse indirme uç noktası kullanılmaz, bunun yerine aşağıdaki ayarlar geçerli olur. \n\n “İndirmeler için dönüştürme formatı” “Doğrudan indir” olarak ayarlanırsa dosyanın bit hızı değiştirilmez.</string>
|
||||
<string name="settings_summary_transcoding_estimate_content_length">Dosya anlık olarak dönüştürüldüğünde, istemci genellikle parçanın süresini göstermez. Bu işlevi destekleyen sunuculardan çalınan parçanın süresini tahmin etmeleri istenebilir,
|
||||
ancak yanıt süreleri daha uzun olabilir.</string>
|
||||
<string name="settings_sync_starred_albums_for_offline_use_title">Çevrimdışı kullanım için yıldızlı albümleri senkronize et</string>
|
||||
<string name="settings_sync_starred_tracks_for_offline_use_summary">Etkinleştirildiğinde, yıldızlı parçalar çevrimdışı kullanım için indirilecektir.</string>
|
||||
<string name="settings_sync_starred_tracks_for_offline_use_title">Çevrimdışı kullanım için yıldızlı parçaları eşitle</string>
|
||||
<string name="settings_theme">Tema</string>
|
||||
|
|
@ -395,6 +401,8 @@
|
|||
<string name="starred_sync_dialog_positive_button">Devam et ve indir</string>
|
||||
<string name="starred_sync_dialog_summary">Yıldızlı parçaların indirilmesi yüksek miktarda veri gerektirebilir.</string>
|
||||
<string name="starred_sync_dialog_title">Yıldızlı parçaları eşitle</string>
|
||||
<string name="starred_album_sync_dialog_summary">Yıldızlı albümleri indirmek yüksek miktarda veri kullanımı gerektirebilir.</string>
|
||||
<string name="starred_album_sync_dialog_title">Yıldızlı albümleri senkronize et</string>
|
||||
<string name="streaming_cache_storage_dialog_sub_summary">Değişikliklerin geçerli olması için uygulamayı yeniden başlatın.</string>
|
||||
<string name="streaming_cache_storage_dialog_summary">Önbelleğe alınmış dosyaların hedefini bir depolamadan diğerine değiştirmek, önceki depolamadaki önbellek dosyalarının silinmesine yol açabilir.</string>
|
||||
<string name="streaming_cache_storage_dialog_title">Depolama seçeneğini seç</string>
|
||||
|
|
@ -433,4 +441,16 @@
|
|||
<string name="undraw_page">unDraw</string>
|
||||
<string name="undraw_thanks">İllüstrasyonlarıyla bu uygulamayı daha güzel hale getirmemize yardımcı olan unDraw’a özel teşekkürler.</string>
|
||||
<string name="undraw_url">https://undraw.co/</string>
|
||||
<string name="home_sync_starred_albums_title">Yıldızlı Albümleri Senkronize Et</string>
|
||||
<string name="widget_label">Tempo Widget</string>
|
||||
<string name="widget_not_playing">Şu an oynatılmıyor</string>
|
||||
<string name="widget_placeholder_subtitle">Tempo’yu aç</string>
|
||||
<string name="widget_time_elapsed_placeholder">0:00</string>
|
||||
<string name="widget_time_duration_placeholder">0:00</string>
|
||||
<string name="widget_content_desc_album_art">Albüm kapağı</string>
|
||||
<string name="widget_content_desc_play_pause">Çal/Duraklat</string>
|
||||
<string name="widget_content_desc_next">Sonraki parça</string>
|
||||
<string name="widget_content_desc_prev">Önceki parça</string>
|
||||
<string name="settings_song_rating">Şarkının yıldız derecelendirmesini göster</string>
|
||||
<string name="settings_sync_starred_albums_for_offline_use_summary">"Etkinleştirildiğinde yıldızlı albümler çevrimdışı kullanım için indirilecek. "</string>
|
||||
</resources>
|
||||
|
|
|
|||
9
app/src/main/res/values/colors_widget.xml
Normal file
9
app/src/main/res/values/colors_widget.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Light theme: bright card with dark content -->
|
||||
<color name="widget_bg">#CCFFFFFF</color>
|
||||
<color name="widget_title">#DE000000</color>
|
||||
<color name="widget_subtitle">#99000000</color>
|
||||
<color name="widget_icon_tint">#DE000000</color>
|
||||
<color name="widget_icon_tint_active">#FF6750A4</color>
|
||||
</resources>
|
||||
6
app/src/main/res/values/integers.xml
Normal file
6
app/src/main/res/values/integers.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="widget_medium_min_height_dp">100</integer>
|
||||
<integer name="widget_large_min_height_dp">160</integer>
|
||||
<integer name="widget_expanded_min_height_dp">220</integer>
|
||||
</resources>
|
||||
|
|
@ -472,6 +472,17 @@
|
|||
<string name="undraw_page">unDraw</string>
|
||||
<string name="undraw_thanks">A special thanks goes to unDraw without whose illustrations we could not have made this application more beautiful.</string>
|
||||
<string name="undraw_url">https://undraw.co/</string>
|
||||
<string name="widget_label">Tempo Widget</string>
|
||||
<string name="widget_not_playing">Not playing</string>
|
||||
<string name="widget_placeholder_subtitle">Open Tempo</string>
|
||||
<string name="widget_time_elapsed_placeholder">0:00</string>
|
||||
<string name="widget_time_duration_placeholder">0:00</string>
|
||||
<string name="widget_content_desc_album_art">Album artwork</string>
|
||||
<string name="widget_content_desc_play_pause">Play or pause</string>
|
||||
<string name="widget_content_desc_next">Next track</string>
|
||||
<string name="widget_content_desc_prev">Previous track</string>
|
||||
<string name="widget_content_desc_shuffle">Toggle shuffle</string>
|
||||
<string name="widget_content_desc_repeat">Change repeat mode</string>
|
||||
<plurals name="home_sync_starred_albums_count">
|
||||
<item quantity="one">%d album to sync</item>
|
||||
<item quantity="other">%d albums to sync</item>
|
||||
|
|
|
|||
10
app/src/main/res/xml/widget_info.xml
Normal file
10
app/src/main/res/xml/widget_info.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="64dp"
|
||||
android:updatePeriodMillis="0"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:initialLayout="@layout/widget_layout_compact"
|
||||
android:previewImage="@drawable/ic_splash_logo"
|
||||
android:previewLayout="@layout/widget_preview_compact"
|
||||
android:widgetCategory="home_screen|keyguard" />
|
||||
|
|
@ -8,6 +8,8 @@ import android.content.Intent
|
|||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.media3.common.*
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
|
|
@ -23,6 +25,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil
|
|||
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
||||
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
|
|
@ -39,6 +42,18 @@ class MediaService : MediaLibraryService() {
|
|||
lateinit var equalizerManager: EqualizerManager
|
||||
|
||||
private var customLayout = ImmutableList.of<CommandButton>()
|
||||
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||
private var widgetUpdateScheduled = false
|
||||
private val widgetUpdateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (!player.isPlaying) {
|
||||
widgetUpdateScheduled = false
|
||||
return
|
||||
}
|
||||
updateWidget()
|
||||
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getEqualizerManager(): EqualizerManager {
|
||||
|
|
@ -80,6 +95,7 @@ class MediaService : MediaLibraryService() {
|
|||
|
||||
override fun onDestroy() {
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
releasePlayer()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
|
@ -260,6 +276,7 @@ class MediaService : MediaLibraryService() {
|
|||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
|
|
@ -282,6 +299,12 @@ class MediaService : MediaLibraryService() {
|
|||
} else {
|
||||
MediaManager.scrobble(player.currentMediaItem, false)
|
||||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
|
|
@ -293,6 +316,7 @@ class MediaService : MediaLibraryService() {
|
|||
MediaManager.scrobble(player.currentMediaItem, true)
|
||||
MediaManager.saveChronology(player.currentMediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
|
|
@ -326,6 +350,9 @@ class MediaService : MediaLibraryService() {
|
|||
mediaLibrarySession.setCustomLayout(customLayout)
|
||||
}
|
||||
})
|
||||
if (player.isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPlayer(player: Player) {
|
||||
|
|
@ -386,5 +413,48 @@ class MediaService : MediaLibraryService() {
|
|||
.build()
|
||||
}
|
||||
|
||||
private fun updateWidget() {
|
||||
val mi = player.currentMediaItem
|
||||
val title = mi?.mediaMetadata?.title?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("title")
|
||||
val artist = mi?.mediaMetadata?.artist?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||
val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId")
|
||||
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
WidgetUpdateManager.updateFromState(
|
||||
this,
|
||||
title ?: "",
|
||||
artist ?: "",
|
||||
album ?: "",
|
||||
coverId,
|
||||
player.isPlaying,
|
||||
player.shuffleModeEnabled,
|
||||
player.repeatMode,
|
||||
position,
|
||||
duration
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleWidgetUpdates() {
|
||||
if (widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
|
||||
widgetUpdateScheduled = true
|
||||
}
|
||||
|
||||
private fun stopWidgetUpdates() {
|
||||
if (!widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
|
||||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
|
||||
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
||||
}
|
||||
|
||||
private fun getMediaSourceFactory() =
|
||||
DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this))
|
||||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import android.app.TaskStackBuilder
|
|||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.media3.cast.CastPlayer
|
||||
import androidx.media3.cast.SessionAvailabilityListener
|
||||
import androidx.media3.common.AudioAttributes
|
||||
|
|
@ -25,6 +27,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil
|
|||
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
||||
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
|
|
@ -49,6 +52,18 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
companion object {
|
||||
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
||||
}
|
||||
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||
private var widgetUpdateScheduled = false
|
||||
private val widgetUpdateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (!player.isPlaying) {
|
||||
widgetUpdateScheduled = false
|
||||
return
|
||||
}
|
||||
updateWidget()
|
||||
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
|
@ -80,6 +95,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
|
||||
override fun onDestroy() {
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
releasePlayer()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
|
@ -161,6 +177,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
|
|
@ -183,6 +200,12 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
} else {
|
||||
MediaManager.scrobble(player.currentMediaItem, false)
|
||||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
|
|
@ -195,6 +218,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
MediaManager.scrobble(player.currentMediaItem, true)
|
||||
MediaManager.saveChronology(player.currentMediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
|
|
@ -230,6 +254,47 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
)
|
||||
}
|
||||
})
|
||||
if (player.isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWidget() {
|
||||
val mi = player.currentMediaItem
|
||||
val title = mi?.mediaMetadata?.title?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("title")
|
||||
val artist = mi?.mediaMetadata?.artist?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||
val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId")
|
||||
|
||||
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
WidgetUpdateManager.updateFromState(
|
||||
this,
|
||||
title ?: "",
|
||||
artist ?: "",
|
||||
album ?: "",
|
||||
coverId,
|
||||
player.isPlaying,
|
||||
player.shuffleModeEnabled,
|
||||
player.repeatMode,
|
||||
position,
|
||||
duration
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleWidgetUpdates() {
|
||||
if (widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
|
||||
widgetUpdateScheduled = true
|
||||
}
|
||||
|
||||
private fun stopWidgetUpdates() {
|
||||
if (!widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
|
||||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
private fun initializeLoadControl(): DefaultLoadControl {
|
||||
|
|
@ -294,3 +359,5 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
player.prepare()
|
||||
}
|
||||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import android.app.TaskStackBuilder
|
|||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.media3.cast.CastPlayer
|
||||
import androidx.media3.cast.SessionAvailabilityListener
|
||||
import androidx.media3.common.AudioAttributes
|
||||
|
|
@ -25,6 +27,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil
|
|||
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
||||
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
|
|
@ -49,6 +52,18 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
companion object {
|
||||
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
||||
}
|
||||
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||
private var widgetUpdateScheduled = false
|
||||
private val widgetUpdateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (!player.isPlaying) {
|
||||
widgetUpdateScheduled = false
|
||||
return
|
||||
}
|
||||
updateWidget()
|
||||
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
|
@ -80,6 +95,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
|
||||
override fun onDestroy() {
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
releasePlayer()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
|
@ -161,6 +177,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
|
|
@ -184,6 +201,12 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
} else {
|
||||
MediaManager.scrobble(player.currentMediaItem, false)
|
||||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
|
|
@ -196,6 +219,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
MediaManager.scrobble(player.currentMediaItem, true)
|
||||
MediaManager.saveChronology(player.currentMediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
|
|
@ -225,6 +249,46 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
Preferences.setRepeatMode(repeatMode)
|
||||
}
|
||||
})
|
||||
if (player.isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWidget() {
|
||||
val mi = player.currentMediaItem
|
||||
val title = mi?.mediaMetadata?.title?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("title")
|
||||
val artist = mi?.mediaMetadata?.artist?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||
val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId")
|
||||
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
WidgetUpdateManager.updateFromState(
|
||||
this,
|
||||
title ?: "",
|
||||
artist ?: "",
|
||||
album ?: "",
|
||||
coverId,
|
||||
player.isPlaying,
|
||||
player.shuffleModeEnabled,
|
||||
player.repeatMode,
|
||||
position,
|
||||
duration
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleWidgetUpdates() {
|
||||
if (widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
|
||||
widgetUpdateScheduled = true
|
||||
}
|
||||
|
||||
private fun stopWidgetUpdates() {
|
||||
if (!widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
|
||||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
private fun initializeLoadControl(): DefaultLoadControl {
|
||||
|
|
@ -288,3 +352,5 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
player.prepare()
|
||||
}
|
||||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue