Add method to ClockPlugin to get preview image.

Bug: 125370285
Test: Checked that preview images still appear in picker app.
Test: Added ViewPreviewerTest
Change-Id: I819d58f621be7b0c4f5e3d0e56d7cb2604c8c770
This commit is contained in:
Robert Snoeberger
2019-03-29 14:25:39 -04:00
parent 97de281b10
commit 2ba20603af
10 changed files with 346 additions and 103 deletions

View File

@@ -28,7 +28,7 @@ import java.util.TimeZone;
public interface ClockPlugin extends Plugin {
String ACTION = "com.android.systemui.action.PLUGIN_CLOCK";
int VERSION = 2;
int VERSION = 3;
/**
* Get the name of the clock face.
@@ -47,6 +47,17 @@ public interface ClockPlugin extends Plugin {
*/
Bitmap getThumbnail();
/**
* Get preview images of clock face to be shown in the picker app.
*
* Preview image should be realistic and show what the clock face will look like on AOD and lock
* screen.
*
* @param width width of the preview image, should be the same as device width in pixels.
* @param height height of the preview image, should be the same as device height in pixels.
*/
Bitmap getPreview(int width, int height);
/**
* Get clock view.
* @return clock view from plugin.

View File

@@ -15,15 +15,19 @@
*/
package com.android.keyguard.clock;
import android.app.WallpaperManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Paint.Style;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextClock;
import com.android.internal.colorextraction.ColorExtractor;
import com.android.keyguard.R;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.plugins.ClockPlugin;
import java.util.TimeZone;
@@ -43,6 +47,16 @@ public class BubbleClockController implements ClockPlugin {
*/
private final LayoutInflater mLayoutInflater;
/**
* Extracts accent color from wallpaper.
*/
private final SysuiColorExtractor mColorExtractor;
/**
* Renders preview from clock view.
*/
private final ViewPreviewer mRenderer = new ViewPreviewer();
/**
* Custom clock shown on AOD screen and behind stack scroller on lock.
*/
@@ -64,11 +78,15 @@ public class BubbleClockController implements ClockPlugin {
/**
* Create a BubbleClockController instance.
*
* @param layoutInflater Inflater used to inflate custom clock views.
* @param res Resources contains title and thumbnail.
* @param inflater Inflater used to inflate custom clock views.
* @param colorExtractor Extracts accent color from wallpaper.
*/
public BubbleClockController(Resources res, LayoutInflater inflater) {
public BubbleClockController(Resources res, LayoutInflater inflater,
SysuiColorExtractor colorExtractor) {
mResources = res;
mLayoutInflater = inflater;
mColorExtractor = colorExtractor;
}
private void createViews() {
@@ -98,6 +116,23 @@ public class BubbleClockController implements ClockPlugin {
return BitmapFactory.decodeResource(mResources, R.drawable.bubble_thumbnail);
}
@Override
public Bitmap getPreview(int width, int height) {
// Use the big clock view for the preview
View view = getBigClockView();
// Initialize state of plugin before generating preview.
setDarkAmount(1f);
setTextColor(Color.WHITE);
ColorExtractor.GradientColors colors = mColorExtractor.getColors(
WallpaperManager.FLAG_LOCK, true);
setColorPalette(colors.supportsDarkText(), colors.getColorPalette());
onTimeTick();
return mRenderer.createPreview(view, width, height);
}
@Override
public View getView() {
if (mLockClockContainer == null) {

View File

@@ -16,29 +16,19 @@
package com.android.keyguard.clock;
import android.annotation.Nullable;
import android.app.WallpaperManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import androidx.annotation.VisibleForTesting;
import com.android.internal.colorextraction.ColorExtractor;
import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dock.DockManager;
@@ -51,8 +41,6 @@ import com.android.systemui.util.InjectionInflationController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -128,7 +116,6 @@ public final class ClockManager {
private final List<ClockChangedListener> mListeners = new ArrayList<>();
private final SysuiColorExtractor mColorExtractor;
private final int mWidth;
private final int mHeight;
@@ -144,17 +131,16 @@ public final class ClockManager {
ContentResolver contentResolver, SettingsWrapper settingsWrapper) {
mContext = context;
mPluginManager = pluginManager;
mColorExtractor = colorExtractor;
mContentResolver = contentResolver;
mSettingsWrapper = settingsWrapper;
Resources res = context.getResources();
LayoutInflater layoutInflater = injectionInflater.injectable(LayoutInflater.from(context));
addClockPlugin(new DefaultClockController(res, layoutInflater));
addClockPlugin(new BubbleClockController(res, layoutInflater));
addClockPlugin(new StretchAnalogClockController(res, layoutInflater));
addClockPlugin(new TypeClockController(res, layoutInflater));
addClockPlugin(new DefaultClockController(res, layoutInflater, colorExtractor));
addClockPlugin(new BubbleClockController(res, layoutInflater, colorExtractor));
addClockPlugin(new StretchAnalogClockController(res, layoutInflater, colorExtractor));
addClockPlugin(new TypeClockController(res, layoutInflater, colorExtractor));
// Store the size of the display for generation of clock preview.
DisplayMetrics dm = res.getDisplayMetrics();
@@ -217,7 +203,7 @@ public final class ClockManager {
.setTitle(plugin.getTitle())
.setId(id)
.setThumbnail(() -> plugin.getThumbnail())
.setPreview(() -> getClockPreview(id))
.setPreview(() -> plugin.getPreview(mWidth, mHeight))
.build());
}
@@ -232,81 +218,6 @@ public final class ClockManager {
}
}
/**
* Generate a realistic preview of a clock face.
* @param clockId ID of clock to use for preview, should be obtained from {@link getClockInfos}.
* Returns null if clockId is not found.
*/
@Nullable
private Bitmap getClockPreview(String clockId) {
FutureTask<Bitmap> task = new FutureTask<>(new Callable<Bitmap>() {
@Override
public Bitmap call() {
Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Config.ARGB_8888);
ClockPlugin plugin = mClocks.get(clockId);
if (plugin == null) {
return null;
}
// Use the big clock view for the preview
View clockView = plugin.getBigClockView();
if (clockView == null) {
return null;
}
// Initialize state of plugin before generating preview.
plugin.setDarkAmount(1f);
plugin.setTextColor(Color.WHITE);
ColorExtractor.GradientColors colors = mColorExtractor.getColors(
WallpaperManager.FLAG_LOCK, true);
plugin.setColorPalette(colors.supportsDarkText(), colors.getColorPalette());
plugin.onTimeTick();
// Draw clock view hierarchy to canvas.
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
dispatchVisibilityAggregated(clockView, true);
clockView.measure(MeasureSpec.makeMeasureSpec(mWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY));
clockView.layout(0, 0, mWidth, mHeight);
clockView.draw(canvas);
return bitmap;
}
});
if (Looper.myLooper() == Looper.getMainLooper()) {
task.run();
} else {
mMainHandler.post(task);
}
try {
return task.get();
} catch (Exception e) {
Log.e(TAG, "Error completing task", e);
return null;
}
}
private void dispatchVisibilityAggregated(View view, boolean isVisible) {
// Similar to View.dispatchVisibilityAggregated implementation.
final boolean thisVisible = view.getVisibility() == View.VISIBLE;
if (thisVisible || !isVisible) {
view.onVisibilityAggregated(isVisible);
}
if (view instanceof ViewGroup) {
isVisible = thisVisible && isVisible;
ViewGroup vg = (ViewGroup) view;
int count = vg.getChildCount();
for (int i = 0; i < count; i++) {
dispatchVisibilityAggregated(vg.getChildAt(i), isVisible);
}
}
}
private void notifyClockChanged(ClockPlugin plugin) {
for (int i = 0; i < mListeners.size(); i++) {
// It probably doesn't make sense to supply the same plugin instances to multiple

View File

@@ -15,15 +15,19 @@
*/
package com.android.keyguard.clock;
import android.app.WallpaperManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Paint.Style;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import com.android.internal.colorextraction.ColorExtractor;
import com.android.keyguard.R;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.plugins.ClockPlugin;
import java.util.TimeZone;
@@ -43,6 +47,16 @@ public class DefaultClockController implements ClockPlugin {
*/
private final LayoutInflater mLayoutInflater;
/**
* Extracts accent color from wallpaper.
*/
private final SysuiColorExtractor mColorExtractor;
/**
* Renders preview from clock view.
*/
private final ViewPreviewer mRenderer = new ViewPreviewer();
/**
* Root view of preview.
*/
@@ -61,11 +75,15 @@ public class DefaultClockController implements ClockPlugin {
/**
* Create a DefaultClockController instance.
*
* @param res Resources contains title and thumbnail.
* @param inflater Inflater used to inflate custom clock views.
* @param colorExtractor Extracts accent color from wallpaper.
*/
public DefaultClockController(Resources res, LayoutInflater inflater) {
public DefaultClockController(Resources res, LayoutInflater inflater,
SysuiColorExtractor colorExtractor) {
mResources = res;
mLayoutInflater = inflater;
mColorExtractor = colorExtractor;
}
private void createViews() {
@@ -89,6 +107,23 @@ public class DefaultClockController implements ClockPlugin {
return BitmapFactory.decodeResource(mResources, R.drawable.default_thumbnail);
}
@Override
public Bitmap getPreview(int width, int height) {
// Use the big clock view for the preview
View view = getBigClockView();
// Initialize state of plugin before generating preview.
setDarkAmount(1f);
setTextColor(Color.WHITE);
ColorExtractor.GradientColors colors = mColorExtractor.getColors(
WallpaperManager.FLAG_LOCK, true);
setColorPalette(colors.supportsDarkText(), colors.getColorPalette());
onTimeTick();
return mRenderer.createPreview(view, width, height);
}
@Override
public View getView() {
return null;

View File

@@ -15,15 +15,19 @@
*/
package com.android.keyguard.clock;
import android.app.WallpaperManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Paint.Style;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextClock;
import com.android.internal.colorextraction.ColorExtractor;
import com.android.keyguard.R;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.plugins.ClockPlugin;
import java.util.TimeZone;
@@ -43,6 +47,16 @@ public class StretchAnalogClockController implements ClockPlugin {
*/
private final LayoutInflater mLayoutInflater;
/**
* Extracts accent color from wallpaper.
*/
private final SysuiColorExtractor mColorExtractor;
/**
* Renders preview from clock view.
*/
private final ViewPreviewer mRenderer = new ViewPreviewer();
/**
* Custom clock shown on AOD screen and behind stack scroller on lock.
*/
@@ -64,11 +78,15 @@ public class StretchAnalogClockController implements ClockPlugin {
/**
* Create a BubbleClockController instance.
*
* @param layoutInflater Inflater used to inflate custom clock views.
* @param res Resources contains title and thumbnail.
* @param inflater Inflater used to inflate custom clock views.
* @param colorExtractor Extracts accent color from wallpaper.
*/
public StretchAnalogClockController(Resources res, LayoutInflater inflater) {
public StretchAnalogClockController(Resources res, LayoutInflater inflater,
SysuiColorExtractor colorExtractor) {
mResources = res;
mLayoutInflater = inflater;
mColorExtractor = colorExtractor;
}
private void createViews() {
@@ -98,6 +116,23 @@ public class StretchAnalogClockController implements ClockPlugin {
return BitmapFactory.decodeResource(mResources, R.drawable.stretch_thumbnail);
}
@Override
public Bitmap getPreview(int width, int height) {
// Use the big clock view for the preview
View view = getBigClockView();
// Initialize state of plugin before generating preview.
setDarkAmount(1f);
setTextColor(Color.WHITE);
ColorExtractor.GradientColors colors = mColorExtractor.getColors(
WallpaperManager.FLAG_LOCK, true);
setColorPalette(colors.supportsDarkText(), colors.getColorPalette());
onTimeTick();
return mRenderer.createPreview(view, width, height);
}
@Override
public View getView() {
if (mView == null) {

View File

@@ -15,14 +15,18 @@
*/
package com.android.keyguard.clock;
import android.app.WallpaperManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Paint.Style;
import android.view.LayoutInflater;
import android.view.View;
import com.android.internal.colorextraction.ColorExtractor;
import com.android.keyguard.R;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.plugins.ClockPlugin;
import java.util.TimeZone;
@@ -42,6 +46,16 @@ public class TypeClockController implements ClockPlugin {
*/
private final LayoutInflater mLayoutInflater;
/**
* Extracts accent color from wallpaper.
*/
private final SysuiColorExtractor mColorExtractor;
/**
* Renders preview from clock view.
*/
private final ViewPreviewer mRenderer = new ViewPreviewer();
/**
* Custom clock shown on AOD screen and behind stack scroller on lock.
*/
@@ -61,11 +75,15 @@ public class TypeClockController implements ClockPlugin {
/**
* Create a TypeClockController instance.
*
* @param res Resources contains title and thumbnail.
* @param inflater Inflater used to inflate custom clock views.
* @param colorExtractor Extracts accent color from wallpaper.
*/
TypeClockController(Resources res, LayoutInflater inflater) {
TypeClockController(Resources res, LayoutInflater inflater,
SysuiColorExtractor colorExtractor) {
mResources = res;
mLayoutInflater = inflater;
mColorExtractor = colorExtractor;
}
private void createViews() {
@@ -95,6 +113,23 @@ public class TypeClockController implements ClockPlugin {
return BitmapFactory.decodeResource(mResources, R.drawable.type_thumbnail);
}
@Override
public Bitmap getPreview(int width, int height) {
// Use the big clock view for the preview
View view = getBigClockView();
// Initialize state of plugin before generating preview.
setDarkAmount(1f);
setTextColor(Color.WHITE);
ColorExtractor.GradientColors colors = mColorExtractor.getColors(
WallpaperManager.FLAG_LOCK, true);
setColorPalette(colors.supportsDarkText(), colors.getColorPalette());
onTimeTick();
return mRenderer.createPreview(view, width, height);
}
@Override
public View getView() {
if (mLockClock == null) {

View File

@@ -0,0 +1,105 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.keyguard.clock;
import android.annotation.Nullable;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* Creates a preview image ({@link Bitmap}) of a {@link View} for a custom clock face.
*/
final class ViewPreviewer {
private static final String TAG = "ViewPreviewer";
/**
* Handler used to run {@link View#draw(Canvas)} on the main thread.
*/
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
/**
* Generate a realistic preview of a clock face.
*
* @param view view is used to generate preview image.
* @param width width of the preview image, should be the same as device width in pixels.
* @param height height of the preview image, should be the same as device height in pixels.
* @return bitmap of view.
*/
@Nullable
Bitmap createPreview(View view, int width, int height) {
if (view == null) {
return null;
}
FutureTask<Bitmap> task = new FutureTask<>(new Callable<Bitmap>() {
@Override
public Bitmap call() {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
// Draw clock view hierarchy to canvas.
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.BLACK);
dispatchVisibilityAggregated(view, true);
view.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
view.layout(0, 0, width, height);
view.draw(canvas);
return bitmap;
}
});
if (Looper.myLooper() == Looper.getMainLooper()) {
task.run();
} else {
mMainHandler.post(task);
}
try {
return task.get();
} catch (Exception e) {
Log.e(TAG, "Error completing task", e);
return null;
}
}
private void dispatchVisibilityAggregated(View view, boolean isVisible) {
// Similar to View.dispatchVisibilityAggregated implementation.
final boolean thisVisible = view.getVisibility() == View.VISIBLE;
if (thisVisible || !isVisible) {
view.onVisibilityAggregated(isVisible);
}
if (view instanceof ViewGroup) {
isVisible = thisVisible && isVisible;
ViewGroup vg = (ViewGroup) view;
int count = vg.getChildCount();
for (int i = 0; i < count; i++) {
dispatchVisibilityAggregated(vg.getChildAt(i), isVisible);
}
}
}
}

View File

@@ -27,10 +27,13 @@ import android.view.ViewGroup;
import android.widget.TextView;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -38,12 +41,15 @@ import org.junit.runner.RunWith;
public final class BubbleClockControllerTest extends SysuiTestCase {
private BubbleClockController mClockController;
@Mock SysuiColorExtractor mMockColorExtractor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
Resources res = getContext().getResources();
LayoutInflater layoutInflater = LayoutInflater.from(getContext());
mClockController = new BubbleClockController(res, layoutInflater);
mClockController = new BubbleClockController(res, layoutInflater, mMockColorExtractor);
}
@Test

View File

@@ -27,10 +27,13 @@ import android.view.ViewGroup;
import android.widget.TextView;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -38,12 +41,16 @@ import org.junit.runner.RunWith;
public final class StretchAnalogClockControllerTest extends SysuiTestCase {
private StretchAnalogClockController mClockController;
@Mock SysuiColorExtractor mMockColorExtractor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
Resources res = getContext().getResources();
LayoutInflater layoutInflater = LayoutInflater.from(getContext());
mClockController = new StretchAnalogClockController(res, layoutInflater);
mClockController = new StretchAnalogClockController(res, layoutInflater,
mMockColorExtractor);
}
@Test

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.keyguard.clock
import android.content.Context
import com.google.common.truth.Truth.assertThat
import android.graphics.Canvas
import android.graphics.Color
import android.testing.AndroidTestingRunner
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidTestingRunner::class)
@SmallTest
class ViewPreviewerTest : SysuiTestCase() {
private lateinit var previewer: ViewPreviewer
private lateinit var view: View
@Before
fun setUp() {
previewer = ViewPreviewer()
view = TestView(context)
}
@Test
fun testCreatePreview() {
val width = 100
val height = 100
// WHEN a preview image is created
val bitmap = previewer.createPreview(view, width, height)
// THEN the bitmap has the expected width and height
assertThat(bitmap.height).isEqualTo(height)
assertThat(bitmap.width).isEqualTo(width)
assertThat(bitmap.getPixel(0, 0)).isEqualTo(Color.RED)
}
class TestView(context: Context) : View(context) {
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawColor(Color.RED)
}
}
}