tempus/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java

277 lines
13 KiB
Java
Raw Normal View History

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;
2025-10-07 21:26:36 -07:00
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;
2025-10-07 21:26:36 -07:00
import androidx.media3.common.C;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionToken;
2025-10-07 21:26:36 -07:00
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;
2025-10-07 21:26:36 -07:00
import java.util.concurrent.ExecutionException;
public final class WidgetUpdateManager {
2025-10-07 21:26:36 -07:00
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 = "";
2025-10-07 21:26:36 -07:00
final TimingInfo timing = createTimingInfo(positionMs, durationMs);
2025-10-07 21:26:36 -07:00
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);
}
}
2025-10-07 21:26:36 -07:00
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);
}
}
2025-10-07 21:26:36 -07:00
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);
2025-10-07 21:26:36 -07:00
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);
}
}
2025-10-07 21:26:36 -07:00
@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) {
fix(widget): refine layouts and progress UX across sizes Compact (4×1) - Reduce root vertical padding so the 4×1 cell yields ~56dp of content height. - Make album art a true square (50×50dp) and center vertically; keeps edges clear of rounded corners. - Tighten timing block: 2dp progress bar; 10sp labels with no extra font padding; prevents elapsed/total text from slipping below the background. - Wrap album art in a 50×50dp FrameLayout with a new 6dp-radius background drawable; soft corners while remaining visually smaller than the widget body. - Mirror the same structure in the preview layout so Studio preview matches on-device rendering. (app/src/main/res/layout/widget_layout_compact.xml, app/src/main/res/drawable/widget_album_art_bg.xml) Large Short (4×2) - Wrap album art in a fixed 90dp square container and enforce a true square crop via centerCrop. - Tighten vertical spacing: thinner progress bar, closer timing row, controls shifted down for better balance. - Keep album/timing text to the left of the controls but retune spacing so the stack stays fully inside the widget bounds. Large (4×3 and up) - Restructure to a vertical stack: header row (album art + text), full-width progress bar, timing row, primary controls, then secondary controls. - Lock album art to a 150dp square; progress bar spans the widget beneath the header to match the new visual hierarchy. Based-on: https://github.com/eddyizm/tempo/commit/cd28ee0764b35f46988fb31e4fe2aaebcdb69851 Co-authored-by: The Firehawk <firehawk@opayq.net> Co-Authored-By: Mücahit Kaya <kaya-mucahit@outlook.com> Co-Authored-By: Firehawk <firehawk@opayq.net>
2025-09-20 21:13:23 +02:00
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
2025-10-07 21:26:36 -07:00
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
WidgetProvider.attachIntents(appCtx, rv, id);
mgr.updateAppWidget(id, rv);
}
2025-10-07 21:26:36 -07:00
}
}
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());
}
2025-10-07 21:26:36 -07:00
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;
}
2025-10-07 21:26:36 -07:00
String elapsed = (safeDuration > 0 || safePosition > 0)
? MusicUtil.getReadableDurationString(safePosition, true)
: null;
String total = safeDuration > 0
? MusicUtil.getReadableDurationString(safeDuration, true)
: null;
2025-10-07 21:26:36 -07:00
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;
}
}
2025-10-07 21:26:36 -07:00
return new TimingInfo(elapsed, total, progress);
}
2025-10-07 21:26:36 -07:00
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);
}
fix(widget): refine layouts and progress UX across sizes Compact (4×1) - Reduce root vertical padding so the 4×1 cell yields ~56dp of content height. - Make album art a true square (50×50dp) and center vertically; keeps edges clear of rounded corners. - Tighten timing block: 2dp progress bar; 10sp labels with no extra font padding; prevents elapsed/total text from slipping below the background. - Wrap album art in a 50×50dp FrameLayout with a new 6dp-radius background drawable; soft corners while remaining visually smaller than the widget body. - Mirror the same structure in the preview layout so Studio preview matches on-device rendering. (app/src/main/res/layout/widget_layout_compact.xml, app/src/main/res/drawable/widget_album_art_bg.xml) Large Short (4×2) - Wrap album art in a fixed 90dp square container and enforce a true square crop via centerCrop. - Tighten vertical spacing: thinner progress bar, closer timing row, controls shifted down for better balance. - Keep album/timing text to the left of the controls but retune spacing so the stack stays fully inside the widget bounds. Large (4×3 and up) - Restructure to a vertical stack: header row (album art + text), full-width progress bar, timing row, primary controls, then secondary controls. - Lock album art to a 150dp square; progress bar spans the widget beneath the header to match the new visual hierarchy. Based-on: https://github.com/eddyizm/tempo/commit/cd28ee0764b35f46988fb31e4fe2aaebcdb69851 Co-authored-by: The Firehawk <firehawk@opayq.net> Co-Authored-By: Mücahit Kaya <kaya-mucahit@outlook.com> Co-Authored-By: Firehawk <firehawk@opayq.net>
2025-09-20 21:13:23 +02:00
}
2025-10-07 21:26:36 -07:00
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);
}
}
2025-10-07 21:26:36 -07:00
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;
}
fix(widget): refine layouts and progress UX across sizes Compact (4×1) - Reduce root vertical padding so the 4×1 cell yields ~56dp of content height. - Make album art a true square (50×50dp) and center vertically; keeps edges clear of rounded corners. - Tighten timing block: 2dp progress bar; 10sp labels with no extra font padding; prevents elapsed/total text from slipping below the background. - Wrap album art in a 50×50dp FrameLayout with a new 6dp-radius background drawable; soft corners while remaining visually smaller than the widget body. - Mirror the same structure in the preview layout so Studio preview matches on-device rendering. (app/src/main/res/layout/widget_layout_compact.xml, app/src/main/res/drawable/widget_album_art_bg.xml) Large Short (4×2) - Wrap album art in a fixed 90dp square container and enforce a true square crop via centerCrop. - Tighten vertical spacing: thinner progress bar, closer timing row, controls shifted down for better balance. - Keep album/timing text to the left of the controls but retune spacing so the stack stays fully inside the widget bounds. Large (4×3 and up) - Restructure to a vertical stack: header row (album art + text), full-width progress bar, timing row, primary controls, then secondary controls. - Lock album art to a 150dp square; progress bar spans the widget beneath the header to match the new visual hierarchy. Based-on: https://github.com/eddyizm/tempo/commit/cd28ee0764b35f46988fb31e4fe2aaebcdb69851 Co-authored-by: The Firehawk <firehawk@opayq.net> Co-Authored-By: Mücahit Kaya <kaya-mucahit@outlook.com> Co-Authored-By: Firehawk <firehawk@opayq.net>
2025-09-20 21:13:23 +02:00
2025-10-07 21:26:36 -07:00
private enum LayoutSize {
COMPACT,
MEDIUM,
LARGE,
EXPANDED
}
2025-10-07 21:26:36 -07:00
private static final class TimingInfo {
final String elapsedText;
final String totalText;
final int progress;
2025-10-07 21:26:36 -07:00
TimingInfo(String elapsedText, String totalText, int progress) {
this.elapsedText = elapsedText;
this.totalText = totalText;
this.progress = progress;
}
}
}