CLOCK - show preview for clock widget

This commit is contained in:
Alex Sim committed 2026-07-05 00:17:50 +02:00
1 parent 33095adfbb
commit 16f6ec6f26
5 files changed
+128 -11

No files matched your search

@@ -1,13 +1,7 @@
package app.murinelauncher.widget.smartspace
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.icu.text.DateFormat
import android.icu.text.DisplayContext
import android.provider.AlarmClock
import android.util.AttributeSet
import android.widget.LinearLayout
import android.widget.TextClock
@@ -82,6 +76,16 @@ class MurineClockView @JvmOverloads constructor(
clock.format24Hour = pattern
}
/**
* Will just cause it to refresh and correctly show the hour (useful for showing widget preview)
*/
fun refreshClockFormat() {
listOfNotNull(clockView, dateText).forEach { tc ->
tc.format12Hour = tc.format12Hour
tc.format24Hour = tc.format24Hour
}
}
fun setTextColor(color: Int) {
clockView?.setTextColor(color)
dateText?.setTextColor(color)
@@ -7,6 +7,7 @@ import android.view.LayoutInflater;
import android.view.View;
import com.android.launcher3.R;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.systemui.plugins.CustomWidgetPlugin;
/**
@@ -29,14 +30,36 @@ public class MurineClockWidgetPlugin implements CustomWidgetPlugin {
parent.addView(clockView);
}
@Override
public boolean useLivePreview() {
return true;
}
@Override
public View createPreviewView(Context context) {
View view = LayoutInflater.from(context)
.inflate(R.layout.murine_clock_widget, null, false);
if (view instanceof MurineClockView murineClock) murineClock.refreshClockFormat();
return view;
}
@Override
public void updateWidgetInfo(AppWidgetProviderInfo info, Context context) {
info.label = context.getString(R.string.smartspace_mode_clock);
if (info instanceof LauncherAppWidgetProviderInfo wpi) {
wpi.forceResizableX = true;
wpi.forceResizableY = true;
// 99 is just unrealistically wide enough in order to always pick the workspace width
wpi.spanX = 99;
wpi.spanY = 1;
wpi.minSpanX = 1;
wpi.minSpanY = 1;
}
info.minResizeWidth = 250;
info.minResizeHeight = 48;
info.minWidth = 250;
info.minHeight = 48;
info.resizeMode = AppWidgetProviderInfo.RESIZE_HORIZONTAL;
info.resizeMode = AppWidgetProviderInfo.RESIZE_HORIZONTAL | AppWidgetProviderInfo.RESIZE_VERTICAL;
info.icon = R.drawable.ic_pref_smartspace_clock;
}
}
@@ -24,6 +24,7 @@ import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
@@ -33,9 +34,11 @@ import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.Log;
import android.util.Size;
import android.view.View;
import android.widget.RemoteViews;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.os.BuildCompat;
@@ -52,6 +55,7 @@ import com.android.launcher3.util.CancellableTask;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.custom.CustomWidgetManager;
import com.android.launcher3.widget.util.WidgetSizes;
import java.util.concurrent.ExecutionException;
@@ -155,6 +159,14 @@ public class DatabaseWidgetPreviewLoader {
// Load the preview image if possible
if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
if (info.isCustomWidget()) {
Bitmap livePreview = generateCustomWidgetPreview(info, maxPreviewWidth, preScaledWidthOut);
if (livePreview != null) {
return livePreview;
}
// Otherwise fall through to the standard (icon) preview.
}
Drawable drawable = null;
if (info.previewImage != 0) {
try {
@@ -176,14 +188,15 @@ public class DatabaseWidgetPreviewLoader {
}
final boolean widgetPreviewExists = (drawable != null);
final int spanX = info.spanX;
final int spanY = info.spanY;
DeviceProfile dp = ActivityContext.lookupContext(mContext).getDeviceProfile();
// Clamp the span to the current grid (and honor min span) so a custom widget that declares
// an intentionally-large "full width" span doesn't produce an oversized preview.
final int spanX = Math.max(1, Math.min(Math.max(info.spanX, info.minSpanX), dp.inv.numColumns));
final int spanY = Math.max(1, Math.min(Math.max(info.spanY, info.minSpanY), dp.inv.numRows));
int previewWidth;
int previewHeight;
DeviceProfile dp = ActivityContext.lookupContext(mContext).getDeviceProfile();
if (widgetPreviewExists && drawable.getIntrinsicWidth() > 0
&& drawable.getIntrinsicHeight() > 0) {
previewWidth = drawable.getIntrinsicWidth();
@@ -273,6 +286,51 @@ public class DatabaseWidgetPreviewLoader {
});
}
/**
* Renders a custom widget's live preview view (supplied by its plugin) to a bitmap. Returns
* {@code null} if the plugin provides no preview view or rendering fails, so the caller can fall
* back to the standard preview. The view is inflated and drawn on the UI thread.
*/
@Nullable
private Bitmap generateCustomWidgetPreview(LauncherAppWidgetProviderInfo info,
int maxPreviewWidth, int[] preScaledWidthOut) {
CustomWidgetManager cwm = CustomWidgetManager.INSTANCE.get(mContext);
DeviceProfile dp = ActivityContext.lookupContext(mContext).getDeviceProfile();
// Clamp the span to the current grid (and honor min span) so a custom widget that declares
// an intentionally-large "full width" span doesn't produce an oversized preview.
int spanX = Math.max(1, Math.min(Math.max(info.spanX, info.minSpanX), dp.inv.numColumns));
int spanY = Math.max(1, Math.min(Math.max(info.spanY, info.minSpanY), dp.inv.numRows));
Size widgetSize = WidgetSizes.getWidgetSizePx(dp, spanX, spanY);
final int width = Math.max(widgetSize.getWidth(), 1);
final int height = Math.max(widgetSize.getHeight(), 1);
if (preScaledWidthOut != null) {
preScaledWidthOut[0] = width;
}
final float scale = width > maxPreviewWidth ? (maxPreviewWidth / (float) width) : 1f;
final int scaledWidth = Math.max((int) (scale * width), 1);
final int scaledHeight = Math.max((int) (scale * height), 1);
try {
return MAIN_EXECUTOR.submit(() -> {
View view = cwm.createCustomWidgetPreview(mContext, info.provider);
if (view == null) {
return null;
}
view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
view.layout(0, 0, width, height);
Bitmap bitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
if (scale != 1f) canvas.scale(scale, scale);
view.draw(canvas);
return bitmap;
}).get();
} catch (Exception e) {
Log.w(TAG, "Failed to render custom widget live preview for " + info.provider, e);
return null;
}
}
private Bitmap generateShortcutPreview(
ShortcutConfigActivityInfo info, int maxWidth, int maxHeight) {
int iconSize = ActivityContext.lookupContext(mContext).getDeviceProfile().allAppsIconSizePx;
@@ -27,6 +27,7 @@ import android.content.Context;
import android.os.Parcel;
import android.os.Process;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -160,6 +161,20 @@ public class CustomWidgetManager implements PluginListener<CustomWidgetPlugin> {
return mCustomWidgets.stream();
}
/**
* Returns the live preview view supplied by the plugin for the given custom widget provider, or
* {@code null} if there is no plugin, it hasn't opted into a live preview, or it supplies none.
* Must be called on the UI thread.
*/
@Nullable
public View createCustomWidgetPreview(Context context, ComponentName provider) {
CustomWidgetPlugin plugin = mPlugins.get(provider);
if (plugin == null || !plugin.useLivePreview()) {
return null;
}
return plugin.createPreviewView(context);
}
/**
* Returns the widget provider in respect to given widget id.
*/
@@ -19,6 +19,7 @@ package com.android.systemui.plugins;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.view.View;
import com.android.systemui.plugins.annotations.ProvidesInterface;
@@ -51,4 +52,20 @@ public interface CustomWidgetPlugin extends Plugin {
* Used to modify a widgets' info.
*/
default void updateWidgetInfo(AppWidgetProviderInfo info, Context context) { }
/**
* Whether this widget should render a live preview (via {@link #createPreviewView}) in the
* picker instead of the default icon preview. Off unless a plugin opts in.
*/
default boolean useLivePreview() {
return false;
}
/**
* Returns the view rendered as the widget's preview in the picker, or {@code null} to fall back
* to the standard icon preview. Called on the UI thread, and only if {@link #useLivePreview()}.
*/
default View createPreviewView(Context context) {
return null;
}
}