Loading weathereffects/build.gradle +2 −1 Original line number Diff line number Diff line Loading @@ -13,7 +13,7 @@ // limitations under the License. buildscript { ext.versions = [ 'gradle' : '7.4.2', 'gradle' : '8.1.0', 'minSdk' : 34, 'targetSdk' : 34, 'compileSdk' : 34, Loading Loading @@ -56,6 +56,7 @@ apply plugin: 'kotlin-android' apply plugin: 'org.jetbrains.kotlin.kapt' android { namespace 'com.google.android.wallpaper.weathereffects' compileSdk versions.compileSdk defaultConfig { Loading weathereffects/graphics/build.gradle +23 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,11 @@ android { java.srcDirs = ["${rootDir}/graphics/src"] assets.srcDirs = ["${rootDir}/graphics/assets"] } androidTest { java.srcDirs = ["${rootDir}/graphics/tests/src"] res.srcDirs = ["${rootDir}/graphics/tests/res"] } } buildTypes { Loading @@ -42,10 +47,28 @@ android { kotlinOptions { jvmTarget = '17' } testOptions { unitTests { includeAndroidResources = true } } } dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' androidTestImplementation "junit:junit:4.13.2" androidTestImplementation "androidx.test:core:1.5.0" androidTestImplementation "androidx.test:rules:1.5.0" androidTestImplementation "androidx.test:runner:1.5.2" androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' androidTestImplementation "com.google.truth:truth:1.1.3" androidTestImplementation "org.mockito:mockito-core:5.10.0" androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.3" androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito-inline-extended:2.28.3" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" } weathereffects/graphics/src/main/java/com/google/android/wallpaper/weathereffects/graphics/FrameBuffer.kt +12 −4 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ import android.graphics.RecordingCanvas import android.graphics.RenderEffect import android.graphics.RenderNode import android.hardware.HardwareBuffer import androidx.annotation.VisibleForTesting import java.time.Duration import java.util.concurrent.Executor import java.util.concurrent.Executors Loading @@ -49,7 +50,7 @@ class FrameBuffer(width: Int, height: Int, format: Int = HardwareBuffer.RGBA_888 } private val executor = Executors.newFixedThreadPool(/* nThreads = */ 1) private val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB) @VisibleForTesting val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB) /** * Recording drawing commands. Loading Loading @@ -81,15 +82,18 @@ class FrameBuffer(width: Int, height: Int, format: Int = HardwareBuffer.RGBA_888 * executor. */ fun tryObtainingImage(onImageReady: (image: Bitmap) -> Unit, callbackExecutor: Executor) { if (renderer.isClosed) return renderer.obtainRenderRequest().setColorSpace(colorSpace).draw(executor) { result -> if (result.status == HardwareBufferRenderer.RenderResult.SUCCESS) { result.fence.await(Duration.ofMillis(3000)) result.fence.await(Duration.ofMillis(RESULT_FENCE_TIME_OUT)) if (!buffer.isClosed) { Bitmap.wrapHardwareBuffer(buffer, colorSpace)?.let { callbackExecutor.execute { onImageReady.invoke(it) } } } } } } /** * Configure the [FrameBuffer] to apply to this RenderNode. This will apply a visual effect to Loading @@ -99,4 +103,8 @@ class FrameBuffer(width: Int, height: Int, format: Int = HardwareBuffer.RGBA_888 * configured RenderEffects. */ fun setRenderEffect(renderEffect: RenderEffect?) = node.setRenderEffect(renderEffect) companion object { const val RESULT_FENCE_TIME_OUT = 3000L } } weathereffects/graphics/tests/src/com/google/android/wallpaper/weathereffects/graphics/FrameBufferTest.kt 0 → 100644 +92 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.google.android.wallpaper.weathereffects.graphics import android.graphics.Bitmap import android.graphics.Color import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.android.wallpaper.weathereffects.graphics.FrameBuffer.Companion.RESULT_FENCE_TIME_OUT import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.MoreExecutors import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class FrameBufferTest { private val executor = MoreExecutors.directExecutor() @Test fun onImageReady_invokesCallback() { val expectedWidth = 1 val expectedHeight = 1 val expectedColor = Color.RED val buffer = FrameBuffer(expectedWidth, expectedHeight) buffer.beginDrawing().drawColor(expectedColor) buffer.endDrawing() val latch = CountDownLatch(1) var bitmap: Bitmap? = null buffer.tryObtainingImage( { bitmap = it latch.countDown() }, executor ) assertThat(latch.await(RESULT_FENCE_TIME_OUT, TimeUnit.MILLISECONDS)).isTrue() assertThat(bitmap).isNotNull() val resultBitmap = bitmap!! assertThat(resultBitmap.width).isEqualTo(expectedWidth) assertThat(resultBitmap.height).isEqualTo(expectedHeight) assertThat(resultBitmap.colorSpace).isEqualTo(buffer.colorSpace) // Color sampling only works on software bitmap. val softwareBitmap = resultBitmap.copy(Bitmap.Config.ARGB_8888, false) assertThat(softwareBitmap.getPixel(0, 0)).isEqualTo(expectedColor) } @Test fun close_onImageReady_doesNotInvokeCallback() { val buffer = FrameBuffer(width = 1, height = 1) buffer.beginDrawing().drawColor(Color.RED) buffer.endDrawing() // Call close before we obtain image. buffer.close() val latch = CountDownLatch(1) var bitmap: Bitmap? = null buffer.tryObtainingImage( { bitmap = it latch.countDown() }, executor ) assertThat(latch.await(RESULT_FENCE_TIME_OUT, TimeUnit.MILLISECONDS)).isFalse() assertThat(bitmap).isNull() } } Loading
weathereffects/build.gradle +2 −1 Original line number Diff line number Diff line Loading @@ -13,7 +13,7 @@ // limitations under the License. buildscript { ext.versions = [ 'gradle' : '7.4.2', 'gradle' : '8.1.0', 'minSdk' : 34, 'targetSdk' : 34, 'compileSdk' : 34, Loading Loading @@ -56,6 +56,7 @@ apply plugin: 'kotlin-android' apply plugin: 'org.jetbrains.kotlin.kapt' android { namespace 'com.google.android.wallpaper.weathereffects' compileSdk versions.compileSdk defaultConfig { Loading
weathereffects/graphics/build.gradle +23 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,11 @@ android { java.srcDirs = ["${rootDir}/graphics/src"] assets.srcDirs = ["${rootDir}/graphics/assets"] } androidTest { java.srcDirs = ["${rootDir}/graphics/tests/src"] res.srcDirs = ["${rootDir}/graphics/tests/res"] } } buildTypes { Loading @@ -42,10 +47,28 @@ android { kotlinOptions { jvmTarget = '17' } testOptions { unitTests { includeAndroidResources = true } } } dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' androidTestImplementation "junit:junit:4.13.2" androidTestImplementation "androidx.test:core:1.5.0" androidTestImplementation "androidx.test:rules:1.5.0" androidTestImplementation "androidx.test:runner:1.5.2" androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' androidTestImplementation "com.google.truth:truth:1.1.3" androidTestImplementation "org.mockito:mockito-core:5.10.0" androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.3" androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito-inline-extended:2.28.3" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" }
weathereffects/graphics/src/main/java/com/google/android/wallpaper/weathereffects/graphics/FrameBuffer.kt +12 −4 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ import android.graphics.RecordingCanvas import android.graphics.RenderEffect import android.graphics.RenderNode import android.hardware.HardwareBuffer import androidx.annotation.VisibleForTesting import java.time.Duration import java.util.concurrent.Executor import java.util.concurrent.Executors Loading @@ -49,7 +50,7 @@ class FrameBuffer(width: Int, height: Int, format: Int = HardwareBuffer.RGBA_888 } private val executor = Executors.newFixedThreadPool(/* nThreads = */ 1) private val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB) @VisibleForTesting val colorSpace = ColorSpace.get(ColorSpace.Named.SRGB) /** * Recording drawing commands. Loading Loading @@ -81,15 +82,18 @@ class FrameBuffer(width: Int, height: Int, format: Int = HardwareBuffer.RGBA_888 * executor. */ fun tryObtainingImage(onImageReady: (image: Bitmap) -> Unit, callbackExecutor: Executor) { if (renderer.isClosed) return renderer.obtainRenderRequest().setColorSpace(colorSpace).draw(executor) { result -> if (result.status == HardwareBufferRenderer.RenderResult.SUCCESS) { result.fence.await(Duration.ofMillis(3000)) result.fence.await(Duration.ofMillis(RESULT_FENCE_TIME_OUT)) if (!buffer.isClosed) { Bitmap.wrapHardwareBuffer(buffer, colorSpace)?.let { callbackExecutor.execute { onImageReady.invoke(it) } } } } } } /** * Configure the [FrameBuffer] to apply to this RenderNode. This will apply a visual effect to Loading @@ -99,4 +103,8 @@ class FrameBuffer(width: Int, height: Int, format: Int = HardwareBuffer.RGBA_888 * configured RenderEffects. */ fun setRenderEffect(renderEffect: RenderEffect?) = node.setRenderEffect(renderEffect) companion object { const val RESULT_FENCE_TIME_OUT = 3000L } }
weathereffects/graphics/tests/src/com/google/android/wallpaper/weathereffects/graphics/FrameBufferTest.kt 0 → 100644 +92 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.google.android.wallpaper.weathereffects.graphics import android.graphics.Bitmap import android.graphics.Color import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.android.wallpaper.weathereffects.graphics.FrameBuffer.Companion.RESULT_FENCE_TIME_OUT import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.MoreExecutors import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class FrameBufferTest { private val executor = MoreExecutors.directExecutor() @Test fun onImageReady_invokesCallback() { val expectedWidth = 1 val expectedHeight = 1 val expectedColor = Color.RED val buffer = FrameBuffer(expectedWidth, expectedHeight) buffer.beginDrawing().drawColor(expectedColor) buffer.endDrawing() val latch = CountDownLatch(1) var bitmap: Bitmap? = null buffer.tryObtainingImage( { bitmap = it latch.countDown() }, executor ) assertThat(latch.await(RESULT_FENCE_TIME_OUT, TimeUnit.MILLISECONDS)).isTrue() assertThat(bitmap).isNotNull() val resultBitmap = bitmap!! assertThat(resultBitmap.width).isEqualTo(expectedWidth) assertThat(resultBitmap.height).isEqualTo(expectedHeight) assertThat(resultBitmap.colorSpace).isEqualTo(buffer.colorSpace) // Color sampling only works on software bitmap. val softwareBitmap = resultBitmap.copy(Bitmap.Config.ARGB_8888, false) assertThat(softwareBitmap.getPixel(0, 0)).isEqualTo(expectedColor) } @Test fun close_onImageReady_doesNotInvokeCallback() { val buffer = FrameBuffer(width = 1, height = 1) buffer.beginDrawing().drawColor(Color.RED) buffer.endDrawing() // Call close before we obtain image. buffer.close() val latch = CountDownLatch(1) var bitmap: Bitmap? = null buffer.tryObtainingImage( { bitmap = it latch.countDown() }, executor ) assertThat(latch.await(RESULT_FENCE_TIME_OUT, TimeUnit.MILLISECONDS)).isFalse() assertThat(bitmap).isNull() } }