From 19298f8ea70723abb71491c38c1b894b413055e8 Mon Sep 17 00:00:00 2001 From: Peter Kalauskas Date: Wed, 16 Aug 2023 10:52:24 -0700 Subject: studiow: remove version defs Version definitions in this gradle project are unessary and cause build warnings. Test: test_gradle_build.sh Bug: 296240278 Change-Id: Ia2caf287b35fdcf95cb0ce5ad952793e090d5011 --- animationlib/build.gradle | 6 ------ 1 file changed, 6 deletions(-) diff --git a/animationlib/build.gradle b/animationlib/build.gradle index 18ae0e1..906bb92 100644 --- a/animationlib/build.gradle +++ b/animationlib/build.gradle @@ -19,12 +19,6 @@ android { manifest.srcFile 'tests/AndroidManifest.xml' } } - compileSdk 33 - - defaultConfig { - minSdk 33 - targetSdk 33 - } lintOptions { abortOnError false -- cgit v1.2.3 From 115a6dec315f9c484cb2054857c034728a04c986 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Wed, 16 Aug 2023 16:30:39 -0700 Subject: Exposing some utility methods in icon lib Bug: 270396209 Test: Verified on device Flag: ENABLE_FORCED_MONO_ICON Change-Id: I85ce30c69254ac609315b1b7d93679db420fa176 --- .../adaptive_icon_drawable_wrapper.xml | 22 ------- iconloaderlib/res/values/colors.xml | 1 - .../android/launcher3/icons/BaseIconFactory.java | 70 ++++++++++++++++++---- .../com/android/launcher3/icons/BitmapInfo.java | 43 ++++++++----- .../launcher3/icons/FastBitmapDrawable.java | 17 ++++++ .../launcher3/icons/FixedScaleDrawable.java | 53 ---------------- 6 files changed, 105 insertions(+), 101 deletions(-) delete mode 100644 iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml delete mode 100644 iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java diff --git a/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml b/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml deleted file mode 100644 index 9f13cf5..0000000 --- a/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - diff --git a/iconloaderlib/res/values/colors.xml b/iconloaderlib/res/values/colors.xml index 8eeafb4..3abaaa1 100644 --- a/iconloaderlib/res/values/colors.xml +++ b/iconloaderlib/res/values/colors.xml @@ -21,7 +21,6 @@ #D3E3FD #0842A0 #D3E3FD - #FFFFFF #f9ab00 diff --git a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java index 704df6f..577bf4e 100644 --- a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java +++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java @@ -29,6 +29,7 @@ import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.DrawableWrapper; import android.graphics.drawable.InsetDrawable; import android.os.Build; import android.os.UserHandle; @@ -52,6 +53,7 @@ import java.util.Objects; public class BaseIconFactory implements AutoCloseable { private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE; + private static final float LEGACY_ICON_SCALE = .7f * (1f / (1 + 2 * getExtraInsetFraction())); public static final int MODE_DEFAULT = 0; public static final int MODE_ALPHA = 1; @@ -309,24 +311,19 @@ public class BaseIconFactory implements AutoCloseable { if (icon == null) { return null; } - float scale = 1f; + float scale; if (shrinkNonAdaptiveIcons && !(icon instanceof AdaptiveIconDrawable)) { - if (mWrapperIcon == null) { - mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper) - .mutate(); - } - AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon; + EmptyWrapper foreground = new EmptyWrapper(); + AdaptiveIconDrawable dr = new AdaptiveIconDrawable( + new ColorDrawable(mWrapperBackgroundColor), foreground); dr.setBounds(0, 0, 1, 1); boolean[] outShape = new boolean[1]; scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape); if (!outShape[0]) { - FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground()); - fsd.setDrawable(icon); - fsd.setScale(scale); + foreground.setDrawable(createScaledDrawable(icon, scale * LEGACY_ICON_SCALE)); icon = dr; scale = getNormalizer().getScale(icon, outIconBounds, null, null); - ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor); } } else { scale = getNormalizer().getScale(icon, outIconBounds, null, null); @@ -336,6 +333,46 @@ public class BaseIconFactory implements AutoCloseable { return icon; } + /** + * Returns a drawable which draws the original drawable at a fixed scale + */ + private Drawable createScaledDrawable(@NonNull Drawable main, float scale) { + float h = main.getIntrinsicHeight(); + float w = main.getIntrinsicWidth(); + float scaleX = scale; + float scaleY = scale; + if (h > w && w > 0) { + scaleX *= w / h; + } else if (w > h && h > 0) { + scaleY *= h / w; + } + scaleX = (1 - scaleX) / 2; + scaleY = (1 - scaleY) / 2; + return new InsetDrawable(main, scaleX, scaleY, scaleX, scaleY); + } + + /** + * Wraps the provided icon in an adaptive icon drawable + */ + public AdaptiveIconDrawable wrapToAdaptiveIcon(@NonNull Drawable icon) { + if (icon instanceof AdaptiveIconDrawable aid) { + return aid; + } else { + EmptyWrapper foreground = new EmptyWrapper(); + AdaptiveIconDrawable dr = new AdaptiveIconDrawable( + new ColorDrawable(mWrapperBackgroundColor), foreground); + dr.setBounds(0, 0, 1, 1); + boolean[] outShape = new boolean[1]; + float scale = getNormalizer().getScale(icon, null, dr.getIconMask(), outShape); + if (!outShape[0]) { + foreground.setDrawable(createScaledDrawable(icon, scale * LEGACY_ICON_SCALE)); + } else { + foreground.setDrawable(createScaledDrawable(icon, 1 - getExtraInsetFraction())); + } + return dr; + } + } + @NonNull protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale) { return createIconBitmap(icon, scale, MODE_DEFAULT); @@ -615,4 +652,17 @@ public class BaseIconFactory implements AutoCloseable { mTextPaint); } } + + private static class EmptyWrapper extends DrawableWrapper { + + EmptyWrapper() { + super(new ColorDrawable()); + } + + @Override + public ConstantState getConstantState() { + Drawable d = getDrawable(); + return d == null ? null : d.getConstantState(); + } + } } diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java index d1ef6f7..a2ba6d4 100644 --- a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java +++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java @@ -16,10 +16,10 @@ package com.android.launcher3.icons; import android.content.Context; -import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; +import android.graphics.drawable.Drawable; import androidx.annotation.IntDef; import androidx.annotation.NonNull; @@ -151,25 +151,38 @@ public class BitmapInfo { protected void applyFlags(Context context, FastBitmapDrawable drawable, @DrawableCreationFlags int creationFlags) { drawable.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f); + drawable.mCreationFlags = creationFlags; if ((creationFlags & FLAG_NO_BADGE) == 0) { - if (badgeInfo != null) { - drawable.setBadge(badgeInfo.newIcon(context, creationFlags)); - } else if ((flags & FLAG_INSTANT) != 0) { - drawable.setBadge(context.getDrawable(drawable.isThemed() - ? R.drawable.ic_instant_app_badge_themed - : R.drawable.ic_instant_app_badge)); - } else if ((flags & FLAG_WORK) != 0) { - drawable.setBadge(context.getDrawable(drawable.isThemed() - ? R.drawable.ic_work_app_badge_themed - : R.drawable.ic_work_app_badge)); - } else if ((flags & FLAG_CLONE) != 0) { - drawable.setBadge(context.getDrawable(drawable.isThemed() - ? R.drawable.ic_clone_app_badge_themed - : R.drawable.ic_clone_app_badge)); + Drawable badge = getBadgeDrawable(context, (creationFlags & FLAG_THEMED) != 0); + if (badge != null) { + drawable.setBadge(badge); } } } + /** + * Returns a drawable representing the badge for this info + */ + @Nullable + public Drawable getBadgeDrawable(Context context, boolean isThemed) { + if (badgeInfo != null) { + return badgeInfo.newIcon(context, isThemed ? FLAG_THEMED : 0); + } else if ((flags & FLAG_INSTANT) != 0) { + return context.getDrawable(isThemed + ? R.drawable.ic_instant_app_badge_themed + : R.drawable.ic_instant_app_badge); + } else if ((flags & FLAG_WORK) != 0) { + return context.getDrawable(isThemed + ? R.drawable.ic_work_app_badge_themed + : R.drawable.ic_work_app_badge); + } else if ((flags & FLAG_CLONE) != 0) { + return context.getDrawable(isThemed + ? R.drawable.ic_clone_app_badge_themed + : R.drawable.ic_clone_app_badge); + } + return null; + } + public static BitmapInfo fromBitmap(@NonNull Bitmap bitmap) { return of(bitmap, 0); } diff --git a/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java index a97c84f..849a2ae 100644 --- a/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java +++ b/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java @@ -17,6 +17,7 @@ package com.android.launcher3.icons; import static com.android.launcher3.icons.BaseIconFactory.getBadgeSizeForIconSize; +import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; import android.animation.ObjectAnimator; @@ -40,6 +41,8 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.core.graphics.ColorUtils; +import com.android.launcher3.icons.BitmapInfo.DrawableCreationFlags; + public class FastBitmapDrawable extends Drawable implements Drawable.Callback { private static final Interpolator ACCEL = new AccelerateInterpolator(); @@ -71,6 +74,8 @@ public class FastBitmapDrawable extends Drawable implements Drawable.Callback { protected boolean mIsDisabled; float mDisabledAlpha = 1f; + @DrawableCreationFlags int mCreationFlags = 0; + // Animator and properties for the fast bitmap drawable's scale @VisibleForTesting protected static final FloatProperty SCALE = new FloatProperty("scale") { @@ -155,6 +160,14 @@ public class FastBitmapDrawable extends Drawable implements Drawable.Callback { return false; } + /** + * Returns true if the drawable was created with theme, even if it doesn't + * support theming itself. + */ + public boolean isCreatedForTheme() { + return isThemed() || (mCreationFlags & FLAG_THEMED) != 0; + } + @Override public void setColorFilter(ColorFilter cf) { mColorFilter = cf; @@ -317,6 +330,7 @@ public class FastBitmapDrawable extends Drawable implements Drawable.Callback { if (mBadge != null) { cs.mBadgeConstantState = mBadge.getConstantState(); } + cs.mCreationFlags = mCreationFlags; return cs; } @@ -395,6 +409,8 @@ public class FastBitmapDrawable extends Drawable implements Drawable.Callback { protected boolean mIsDisabled; private ConstantState mBadgeConstantState; + @DrawableCreationFlags int mCreationFlags = 0; + public FastBitmapConstantState(Bitmap bitmap, int color) { mBitmap = bitmap; mIconColor = color; @@ -411,6 +427,7 @@ public class FastBitmapDrawable extends Drawable implements Drawable.Callback { if (mBadgeConstantState != null) { drawable.setBadge(mBadgeConstantState.newDrawable()); } + drawable.mCreationFlags = mCreationFlags; return drawable; } diff --git a/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java deleted file mode 100644 index 516965e..0000000 --- a/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.android.launcher3.icons; - -import android.content.res.Resources; -import android.content.res.Resources.Theme; -import android.graphics.Canvas; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.DrawableWrapper; -import android.util.AttributeSet; - -import org.xmlpull.v1.XmlPullParser; - -/** - * Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount. - */ -public class FixedScaleDrawable extends DrawableWrapper { - - // TODO b/33553066 use the constant defined in MaskableIconDrawable - private static final float LEGACY_ICON_SCALE = .7f * .6667f; - private float mScaleX, mScaleY; - - public FixedScaleDrawable() { - super(new ColorDrawable()); - mScaleX = LEGACY_ICON_SCALE; - mScaleY = LEGACY_ICON_SCALE; - } - - @Override - public void draw(Canvas canvas) { - int saveCount = canvas.save(); - canvas.scale(mScaleX, mScaleY, - getBounds().exactCenterX(), getBounds().exactCenterY()); - super.draw(canvas); - canvas.restoreToCount(saveCount); - } - - @Override - public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { } - - @Override - public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { } - - public void setScale(float scale) { - float h = getIntrinsicHeight(); - float w = getIntrinsicWidth(); - mScaleX = scale * LEGACY_ICON_SCALE; - mScaleY = scale * LEGACY_ICON_SCALE; - if (h > w && w > 0) { - mScaleX *= w / h; - } else if (w > h && h > 0) { - mScaleY *= h / w; - } - } -} -- cgit v1.2.3 From ef8a1965953ca40dcda8d5f3cb2698feefb149ee Mon Sep 17 00:00:00 2001 From: Johannes Gallmann Date: Tue, 29 Aug 2023 10:11:23 +0000 Subject: Convert animation-lib tests to bivalent tests Bug: 297950085 Test: atest animationlib_robo_tests, atest animationlib_tests Change-Id: I3fcbca9f362eaaf6451d93f5013ed4a1f3ec66d9 --- animationlib/Android.bp | 45 ++++++++++++++++++---- animationlib/build.gradle | 3 +- .../robolectric/config/robolectric.properties | 2 + .../animation/robolectric/ShadowAnimationUtils2.kt | 12 ++++++ .../app/animation/InterpolatorResourcesTest.kt | 6 +-- .../app/animation/InterpolatorsAndroidXTest.kt | 4 +- 6 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 animationlib/tests/robolectric/config/robolectric.properties create mode 100644 animationlib/tests/robolectric/src/com/android/app/animation/robolectric/ShadowAnimationUtils2.kt diff --git a/animationlib/Android.bp b/animationlib/Android.bp index 5417001..2e0de9a 100644 --- a/animationlib/Android.bp +++ b/animationlib/Android.bp @@ -40,23 +40,52 @@ android_library { platform_apis: false } -android_test { - name: "animationlib_tests", - manifest: "tests/AndroidManifest.xml", - +android_library { + name: "animationlib-tests-base", + libs: [ + "android.test.base", + "androidx.test.core", + ], static_libs: [ "animationlib", "androidx.test.ext.junit", "androidx.test.rules", "testables", + ] +} + +android_app { + name: "TestAnimationLibApp", + platform_apis: true, + static_libs: [ + "animationlib-tests-base", + ] +} + +android_robolectric_test { + enabled: true, + name: "animationlib_robo_tests", + srcs: [ + "tests/src/**/*.kt", + "tests/robolectric/src/**/*.kt" ], - libs: [ - "android.test.base", + java_resource_dirs: ["tests/robolectric/config"], + instrumentation_for: "TestAnimationLibApp", + upstream: true, +} + +android_test { + name: "animationlib_tests", + manifest: "tests/AndroidManifest.xml", + + static_libs: [ + "animationlib-tests-base", ], srcs: [ - "**/*.java", - "**/*.kt" + "tests/src/**/*.java", + "tests/src/**/*.kt" ], kotlincflags: ["-Xjvm-default=all"], test_suites: ["general-tests"], } + diff --git a/animationlib/build.gradle b/animationlib/build.gradle index 906bb92..bd5c575 100644 --- a/animationlib/build.gradle +++ b/animationlib/build.gradle @@ -15,7 +15,7 @@ android { manifest.srcFile 'AndroidManifest.xml' } androidTest { - java.srcDirs = ["tests/src"] + java.srcDirs = ["tests/src", "tests/robolectric/src"] manifest.srcFile 'tests/AndroidManifest.xml' } } @@ -37,6 +37,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0" implementation "androidx.core:core-animation:1.0.0-alpha02" implementation "androidx.core:core-ktx:1.9.0" + androidTestImplementation libs.robolectric androidTestImplementation "androidx.test.ext:junit:1.1.3" androidTestImplementation "androidx.test:rules:1.4.0" } diff --git a/animationlib/tests/robolectric/config/robolectric.properties b/animationlib/tests/robolectric/config/robolectric.properties new file mode 100644 index 0000000..527eab6 --- /dev/null +++ b/animationlib/tests/robolectric/config/robolectric.properties @@ -0,0 +1,2 @@ +sdk=NEWEST_SDK +shadows=com.android.app.animation.robolectric.ShadowAnimationUtils2 diff --git a/animationlib/tests/robolectric/src/com/android/app/animation/robolectric/ShadowAnimationUtils2.kt b/animationlib/tests/robolectric/src/com/android/app/animation/robolectric/ShadowAnimationUtils2.kt new file mode 100644 index 0000000..c3e74ee --- /dev/null +++ b/animationlib/tests/robolectric/src/com/android/app/animation/robolectric/ShadowAnimationUtils2.kt @@ -0,0 +1,12 @@ +package com.android.app.animation.robolectric + +import android.view.animation.AnimationUtils +import org.robolectric.annotation.Implements +import org.robolectric.shadows.ShadowAnimationUtils + +/** + * This shadow overwrites [ShadowAnimationUtils] and ensures that the real implementation of + * [AnimationUtils] is used in tests. + */ +@Implements(AnimationUtils::class) +class ShadowAnimationUtils2 diff --git a/animationlib/tests/src/com/android/app/animation/InterpolatorResourcesTest.kt b/animationlib/tests/src/com/android/app/animation/InterpolatorResourcesTest.kt index ed4670e..f54493e 100644 --- a/animationlib/tests/src/com/android/app/animation/InterpolatorResourcesTest.kt +++ b/animationlib/tests/src/com/android/app/animation/InterpolatorResourcesTest.kt @@ -3,23 +3,23 @@ package com.android.app.animation import android.annotation.InterpolatorRes import android.content.Context import android.view.animation.AnimationUtils +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry import junit.framework.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.JUnit4 @SmallTest -@RunWith(JUnit4::class) +@RunWith(AndroidJUnit4::class) class InterpolatorResourcesTest { private lateinit var context: Context @Before fun setup() { - context = InstrumentationRegistry.getInstrumentation().context + context = InstrumentationRegistry.getInstrumentation().targetContext } @Test diff --git a/animationlib/tests/src/com/android/app/animation/InterpolatorsAndroidXTest.kt b/animationlib/tests/src/com/android/app/animation/InterpolatorsAndroidXTest.kt index ffa706e..bed06cd 100644 --- a/animationlib/tests/src/com/android/app/animation/InterpolatorsAndroidXTest.kt +++ b/animationlib/tests/src/com/android/app/animation/InterpolatorsAndroidXTest.kt @@ -16,18 +16,18 @@ package com.android.app.animation +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import java.lang.reflect.Modifier import junit.framework.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.JUnit4 private const val ANDROIDX_ANIM_PACKAGE_NAME = "androidx.core.animation." private const val ANDROID_ANIM_PACKAGE_NAME = "android.view.animation." @SmallTest -@RunWith(JUnit4::class) +@RunWith(AndroidJUnit4::class) class InterpolatorsAndroidXTest { @Test -- cgit v1.2.3 From d4cbfb7373e062eef733560e24a43137beb268b4 Mon Sep 17 00:00:00 2001 From: Stefan Andonian Date: Thu, 24 Aug 2023 20:50:12 +0000 Subject: Disable ViewCapture in Release builds Bug: 295447707 Test: Manually tested. Change-Id: Iaa5faec2ecfb93f8454146f963f7a0e2caecec29 --- .../com/android/app/viewcapture/NoOpViewCapture.kt | 21 +++++++++++++++++++++ .../app/viewcapture/SettingsAwareViewCapture.kt | 1 + .../com/android/app/viewcapture/ViewCapture.java | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 viewcapturelib/src/com/android/app/viewcapture/NoOpViewCapture.kt diff --git a/viewcapturelib/src/com/android/app/viewcapture/NoOpViewCapture.kt b/viewcapturelib/src/com/android/app/viewcapture/NoOpViewCapture.kt new file mode 100644 index 0000000..2b86f35 --- /dev/null +++ b/viewcapturelib/src/com/android/app/viewcapture/NoOpViewCapture.kt @@ -0,0 +1,21 @@ +package com.android.app.viewcapture + +import android.media.permission.SafeCloseable +import android.os.HandlerThread +import android.view.View +import android.view.Window + +/** + * We don't want to enable the ViewCapture for release builds, since it currently only serves + * 1p apps, and has memory / cpu load that we don't want to risk negatively impacting release builds + */ +class NoOpViewCapture: ViewCapture(0, 0, + createAndStartNewLooperExecutor("NoOpViewCapture", HandlerThread.MIN_PRIORITY)) { + override fun startCapture(view: View?, name: String?): SafeCloseable { + return SafeCloseable { } + } + + override fun startCapture(window: Window?): SafeCloseable { + return SafeCloseable { } + } +} \ No newline at end of file diff --git a/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt b/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt index 9a857c3..ce1dfe8 100644 --- a/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt +++ b/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt @@ -89,6 +89,7 @@ internal constructor(private val context: Context, executor: Executor) @JvmStatic fun getInstance(context: Context): ViewCapture = when { INSTANCE != null -> INSTANCE!! + !android.os.Build.IS_DEBUGGABLE -> NoOpViewCapture() Looper.myLooper() == Looper.getMainLooper() -> SettingsAwareViewCapture( context.applicationContext, createAndStartNewLooperExecutor("SAViewCapture", diff --git a/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java index bbd797e..507c5a8 100644 --- a/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java +++ b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java @@ -173,7 +173,7 @@ public abstract class ViewCapture { } @AnyThread - public void dumpTo(OutputStream os, Context context) + protected void dumpTo(OutputStream os, Context context) throws InterruptedException, ExecutionException, IOException { if (mIsEnabled) getExportedData(context).writeTo(os); } -- cgit v1.2.3 From 05775c58c0e890bbacf1ccb5b479d2c31612a916 Mon Sep 17 00:00:00 2001 From: Yein Jo Date: Wed, 13 Sep 2023 22:12:17 +0000 Subject: Copy Torus library to frameworks/libs/systemui/toruslib. - A module that depends on Filament is not copied as it's not used for now. - Missing license headers were added to a few files. - SigningConfig is removed. Bug: 290939683 Test: mmm, gradle Change-Id: If37f332b7b0a3b7ed608a1133519ca80b591850b --- toruslib/Android.bp | 52 +++ toruslib/OWNERS | 4 + toruslib/build.gradle | 90 +++++ toruslib/gradle.properties | 24 ++ toruslib/lib-torus/build.gradle | 64 +++ toruslib/lib-torus/gradle.properties | 20 + toruslib/lib-torus/src/main/AndroidManifest.xml | 17 + toruslib/settings.gradle | 47 +++ toruslib/torus-core/build.gradle | 13 + toruslib/torus-core/consumer-rules.pro | 0 toruslib/torus-core/proguard-rules.pro | 21 + toruslib/torus-core/src/main/AndroidManifest.xml | 17 + .../torus/core/activity/TorusViewerActivity.kt | 120 ++++++ .../torus/core/app/KeyguardLockController.kt | 101 +++++ .../core/content/ConfigurationChangeListener.kt | 26 ++ .../android/torus/core/engine/TorusEngine.kt | 62 +++ .../core/engine/listener/TorusTouchListener.kt | 33 ++ .../torus/core/extensions/ConfigurationExt.kt | 28 ++ .../google/android/torus/core/geometry/Vertex.kt | 46 +++ .../android/torus/core/power/FpsThrottler.kt | 135 +++++++ .../android/torus/core/time/TimeController.kt | 79 ++++ .../android/torus/core/wallpaper/LiveWallpaper.kt | 441 +++++++++++++++++++++ .../listener/LiveWallpaperEventListener.kt | 110 +++++ .../listener/LiveWallpaperKeyguardEventListener.kt | 24 ++ toruslib/torus-framework-canvas/build.gradle | 18 + .../src/main/AndroidManifest.xml | 17 + .../torus/canvas/engine/CanvasWallpaperEngine.kt | 329 +++++++++++++++ toruslib/torus-math/build.gradle | 13 + toruslib/torus-math/src/main/AndroidManifest.xml | 17 + .../google/android/torus/math/AffineTransform.kt | 177 +++++++++ .../com/google/android/torus/math/MathUtils.kt | 181 +++++++++ .../google/android/torus/math/MatrixTransform.kt | 38 ++ .../android/torus/math/RotationQuaternion.kt | 198 +++++++++ .../android/torus/math/SphericalTransform.kt | 313 +++++++++++++++ .../java/com/google/android/torus/math/Vector2.kt | 197 +++++++++ .../java/com/google/android/torus/math/Vector3.kt | 226 +++++++++++ toruslib/torus-utils/build.gradle | 17 + toruslib/torus-utils/src/main/AndroidManifest.xml | 15 + .../com/google/android/torus/utils/BitmapUtils.kt | 71 ++++ .../android/torus/utils/animation/EasingUtils.kt | 66 +++ .../utils/broadcast/BroadcastEventController.kt | 92 +++++ .../torus/utils/broadcast/PowerSaveController.kt | 79 ++++ .../torus/utils/content/ResourcesManager.kt | 93 +++++ .../utils/display/DisplayOrientationController.kt | 124 ++++++ .../android/torus/utils/display/DisplaySizeType.kt | 93 +++++ .../android/torus/utils/display/DisplayUtils.kt | 139 +++++++ .../android/torus/utils/extensions/ActivityExt.kt | 56 +++ .../torus/utils/extensions/AssetManagerExt.kt | 71 ++++ .../android/torus/utils/extensions/SizeExt.kt | 52 +++ .../torus/utils/interaction/Gyro2dController.kt | 343 ++++++++++++++++ .../torus/utils/interaction/HingeController.kt | 160 ++++++++ .../torus/utils/wallpaper/WallpaperUtils.kt | 57 +++ toruslib/torus-wallpaper-settings/build.gradle | 19 + .../src/main/AndroidManifest.xml | 17 + .../inlinecontrol/BaseSliceConfigProvider.kt | 64 +++ .../settings/inlinecontrol/ColorChipsRowBuilder.kt | 233 +++++++++++ .../settings/inlinecontrol/InputRangeRowBuilder.kt | 70 ++++ .../SingleSelectionRowConfigProvider.kt | 101 +++++ .../inlinecontrol/SliceConfigController.kt | 52 +++ .../storage/CustomizedSharedPreferences.kt | 250 ++++++++++++ .../src/main/res/values/dimens.xml | 11 + .../src/main/res/values/strings.xml | 4 + 62 files changed, 5647 insertions(+) create mode 100644 toruslib/Android.bp create mode 100644 toruslib/OWNERS create mode 100644 toruslib/build.gradle create mode 100644 toruslib/gradle.properties create mode 100644 toruslib/lib-torus/build.gradle create mode 100644 toruslib/lib-torus/gradle.properties create mode 100644 toruslib/lib-torus/src/main/AndroidManifest.xml create mode 100644 toruslib/settings.gradle create mode 100644 toruslib/torus-core/build.gradle create mode 100644 toruslib/torus-core/consumer-rules.pro create mode 100644 toruslib/torus-core/proguard-rules.pro create mode 100644 toruslib/torus-core/src/main/AndroidManifest.xml create mode 100644 toruslib/torus-core/src/main/java/com/google/android/torus/core/activity/TorusViewerActivity.kt create mode 100644 toruslib/torus-core/src/main/java/com/google/android/torus/core/app/KeyguardLockController.kt create mode 100644 toruslib/torus-core/src/main/java/com/google/android/torus/core/content/ConfigurationChangeListener.kt create mode 100644 toruslib/torus-core/src/main/java/com/google/android/torus/core/engine/TorusEngine.kt create mode 100644 toruslib/torus-core/src/main/java/com/google/android/torus/core/engine/listener/TorusTouchListener.kt create mode 100644 toruslib/torus-core/src/main/java/com/google/android/torus/core/extensions/ConfigurationExt.kt create mode 100644 toruslib/torus-core/src/main/java/com/google/android/torus/core/geometry/Vertex.kt create mode 100644 toruslib/torus-core/src/main/java/com/google/android/torus/core/power/FpsThrottler.kt create mode 100644 toruslib/torus-core/src/main/java/com/google/android/torus/core/time/TimeController.kt create mode 100644 toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt create mode 100644 toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/listener/LiveWallpaperEventListener.kt create mode 100644 toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/listener/LiveWallpaperKeyguardEventListener.kt create mode 100644 toruslib/torus-framework-canvas/build.gradle create mode 100644 toruslib/torus-framework-canvas/src/main/AndroidManifest.xml create mode 100644 toruslib/torus-framework-canvas/src/main/java/com/google/android/torus/canvas/engine/CanvasWallpaperEngine.kt create mode 100644 toruslib/torus-math/build.gradle create mode 100644 toruslib/torus-math/src/main/AndroidManifest.xml create mode 100644 toruslib/torus-math/src/main/java/com/google/android/torus/math/AffineTransform.kt create mode 100644 toruslib/torus-math/src/main/java/com/google/android/torus/math/MathUtils.kt create mode 100644 toruslib/torus-math/src/main/java/com/google/android/torus/math/MatrixTransform.kt create mode 100644 toruslib/torus-math/src/main/java/com/google/android/torus/math/RotationQuaternion.kt create mode 100644 toruslib/torus-math/src/main/java/com/google/android/torus/math/SphericalTransform.kt create mode 100644 toruslib/torus-math/src/main/java/com/google/android/torus/math/Vector2.kt create mode 100644 toruslib/torus-math/src/main/java/com/google/android/torus/math/Vector3.kt create mode 100644 toruslib/torus-utils/build.gradle create mode 100644 toruslib/torus-utils/src/main/AndroidManifest.xml create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/BitmapUtils.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/animation/EasingUtils.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/broadcast/BroadcastEventController.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/broadcast/PowerSaveController.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/content/ResourcesManager.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/display/DisplayOrientationController.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/display/DisplaySizeType.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/display/DisplayUtils.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/extensions/ActivityExt.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/extensions/AssetManagerExt.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/extensions/SizeExt.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/interaction/Gyro2dController.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/interaction/HingeController.kt create mode 100644 toruslib/torus-utils/src/main/java/com/google/android/torus/utils/wallpaper/WallpaperUtils.kt create mode 100644 toruslib/torus-wallpaper-settings/build.gradle create mode 100644 toruslib/torus-wallpaper-settings/src/main/AndroidManifest.xml create mode 100755 toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/BaseSliceConfigProvider.kt create mode 100755 toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/ColorChipsRowBuilder.kt create mode 100644 toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/InputRangeRowBuilder.kt create mode 100644 toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/SingleSelectionRowConfigProvider.kt create mode 100644 toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/SliceConfigController.kt create mode 100755 toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/storage/CustomizedSharedPreferences.kt create mode 100755 toruslib/torus-wallpaper-settings/src/main/res/values/dimens.xml create mode 100644 toruslib/torus-wallpaper-settings/src/main/res/values/strings.xml diff --git a/toruslib/Android.bp b/toruslib/Android.bp new file mode 100644 index 0000000..94763b7 --- /dev/null +++ b/toruslib/Android.bp @@ -0,0 +1,52 @@ +// Copyright (C) 2023 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 { + default_applicable_licenses: [ + "Android-Apache-2.0", + ], +} + +android_library { + name: "toruslib", + srcs: [ + "torus-core/src/**/*.java", + "torus-core/src/**/*.kt", + "torus-framework-canvas/**/*.java", + "torus-framework-canvas/**/*.kt", + "torus-math/src/**/*.java", + "torus-math/src/**/*.kt", + "torus-utils/src/**/*.java", + "torus-utils/src/**/*.kt", + "torus-wallpaper-settings/src/**/*.java", + "torus-wallpaper-settings/src/**/*.kt", + ], + static_libs: [ + "androidx.slice_slice-core", + "androidx.slice_slice-builders", + "androidx.core_core-ktx", + "androidx.appcompat_appcompat", + ], + resource_dirs: [ + "torus-wallpaper-settings/src/main/res", + ], + asset_dirs: [ + ], + manifest: "lib-torus/src/main/AndroidManifest.xml", + optimize: { + enabled: true, + }, + sdk_version: "system_current", +} + diff --git a/toruslib/OWNERS b/toruslib/OWNERS new file mode 100644 index 0000000..5b9b5c0 --- /dev/null +++ b/toruslib/OWNERS @@ -0,0 +1,4 @@ +yeinj@google.com +michelcomin@google.com +shanh@google.com +dupin@google.com \ No newline at end of file diff --git a/toruslib/build.gradle b/toruslib/build.gradle new file mode 100644 index 0000000..ecbe754 --- /dev/null +++ b/toruslib/build.gradle @@ -0,0 +1,90 @@ +// Copyright (C) 2023 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. + +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext.versions = [ + 'minSdk' : 31, + 'targetSdk' : 34, + 'compileSdk' : 34, + 'buildTools' : '29.0.3', + 'kotlin' : '1.6.21', + 'ktx' : '1.5.0-beta02', + 'material' : '1.2.1', + 'appcompat' : '1.3.0', + 'androidXLib': '1.1.0-alpha02' + ] + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:7.4.2" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +subprojects { + if (name.startsWith("torus")) { + version = VERSION_NAME + group = GROUP + + apply plugin: 'com.android.library' + apply plugin: 'kotlin-android' + + android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.targetSdk + } + + buildTypes { + release { + minifyEnabled false + consumerProguardFiles 'lib-proguard-rules.txt' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + } + + dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin" + implementation "androidx.core:core-ktx:$versions.ktx" + implementation "androidx.appcompat:appcompat:$versions.appcompat" + } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/toruslib/gradle.properties b/toruslib/gradle.properties new file mode 100644 index 0000000..0fd6d84 --- /dev/null +++ b/toruslib/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official + +VERSION_NAME=1.1.5 +GROUP=com.google.android.libraries.graphics.torus \ No newline at end of file diff --git a/toruslib/lib-torus/build.gradle b/toruslib/lib-torus/build.gradle new file mode 100644 index 0000000..7fd8263 --- /dev/null +++ b/toruslib/lib-torus/build.gradle @@ -0,0 +1,64 @@ +// Copyright (C) 2023 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. + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.targetSdk + } + + buildTypes { + release { + minifyEnabled true + consumerProguardFiles 'lib-proguard-rules.txt' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main { + java.srcDirs += '../torus-core/src/main/java' + java.srcDirs += '../torus-framework-canvas/src/main/java' + java.srcDirs += '../torus-math/src/main/java' + java.srcDirs += '../torus-utils/src/main/java' + java.srcDirs += '../torus-wallpaper-settings/src/main/java' + java.srcDirs += '../torus-wallpaper-settings/src/main/gen' + + res.srcDirs += '../torus-wallpaper-settings/src/main/res' + } + } +} + +dependencies { + implementation "androidx.appcompat:appcompat:$versions.appcompat" + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin" + + implementation "androidx.core:core-ktx:$versions.ktx" + implementation "androidx.slice:slice-builders:$versions.androidXLib" + implementation "androidx.slice:slice-core:$versions.androidXLib" +} diff --git a/toruslib/lib-torus/gradle.properties b/toruslib/lib-torus/gradle.properties new file mode 100644 index 0000000..7b00076 --- /dev/null +++ b/toruslib/lib-torus/gradle.properties @@ -0,0 +1,20 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# Convert third-party libraries to use AndroidX +android.useAndroidX=true +android.enableJetifier=true diff --git a/toruslib/lib-torus/src/main/AndroidManifest.xml b/toruslib/lib-torus/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3c68a96 --- /dev/null +++ b/toruslib/lib-torus/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + diff --git a/toruslib/settings.gradle b/toruslib/settings.gradle new file mode 100644 index 0000000..74e760b --- /dev/null +++ b/toruslib/settings.gradle @@ -0,0 +1,47 @@ +// Copyright (C) 2023 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. + +include ':torus-wallpaper-settings' +include ':torus-math' +include ':torus-utils' +include ':torus-framework-canvas' +include ':torus-core' +include ':lib-torus' + +rootProject.name = "Live Wallpapers Framework" + +// Reads wallpaper.build.properties from the project and loads the build +// settings if it exists +def wallpaperBuildProperties = file("wallpaper.build.properties") +def localProperties = new Properties() +if (wallpaperBuildProperties.canRead()) + wallpaperBuildProperties.withInputStream { localProperties.load(it) } + +def getWallpaperProperty = { name, defaultValue -> + if (localProperties.getProperty(name)) { + return localProperties.getProperty(name) + } + return defaultValue +} + +gradle.ext.wallpaperMinSdkVersion = getWallpaperProperty('wallpaperMinSdkVersion', 28) +gradle.ext.wallpaperTargetSdkVersion = getWallpaperProperty('wallpaperTargetSdkVersion', 30) +gradle.ext.compileSdkVersion = getWallpaperProperty('compileSdkVersion', 30) +gradle.ext.buildToolsVersion = getWallpaperProperty('buildToolsVersion', '29.0.3') +gradle.ext.kotlinVersion = getWallpaperProperty('kotlinVersion', '1.4.32') +gradle.ext.coreKtxVersion = getWallpaperProperty('coreKtxVersion', '1.6.0') +gradle.ext.androidXLibVersion_alpha = getWallpaperProperty( + 'androidXLibVersion_alpha', '1.1.0-alpha01') + +gradle.ext.isPixel = false diff --git a/toruslib/torus-core/build.gradle b/toruslib/torus-core/build.gradle new file mode 100644 index 0000000..7dcad1d --- /dev/null +++ b/toruslib/torus-core/build.gradle @@ -0,0 +1,13 @@ +// Copyright (C) 2023 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. diff --git a/toruslib/torus-core/consumer-rules.pro b/toruslib/torus-core/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/toruslib/torus-core/proguard-rules.pro b/toruslib/torus-core/proguard-rules.pro new file mode 100644 index 0000000..cdc313f --- /dev/null +++ b/toruslib/torus-core/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/toruslib/torus-core/src/main/AndroidManifest.xml b/toruslib/torus-core/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3c68a96 --- /dev/null +++ b/toruslib/torus-core/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/activity/TorusViewerActivity.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/activity/TorusViewerActivity.kt new file mode 100644 index 0000000..b784545 --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/activity/TorusViewerActivity.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 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.torus.core.activity + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.ActivityInfo +import android.content.res.Configuration +import android.os.Bundle +import android.view.SurfaceHolder +import android.view.SurfaceView +import androidx.appcompat.app.AppCompatActivity +import com.google.android.torus.core.content.ConfigurationChangeListener +import com.google.android.torus.core.engine.TorusEngine +import com.google.android.torus.core.engine.listener.TorusTouchListener + +/** + * Helper activity to show a [TorusEngine] into a [SurfaceView]. To use it, you should override + * [getWallpaperEngine] and return an instance of your[TorusEngine] that will draw inside the + * given surface. + * + * Note: [TorusViewerActivity] subclasses must include the following attribute/s + * in the AndroidManifest.xml: + * - android:configChanges="uiMode" + */ +abstract class TorusViewerActivity : AppCompatActivity() { + private lateinit var wallpaperEngine: TorusEngine + private lateinit var surfaceView: SurfaceView + + /** + * Must be implemented to return a new instance of [TorusEngine]. + */ + abstract fun getWallpaperEngine(context: Context, surfaceView: SurfaceView): TorusEngine + + @SuppressLint("ClickableViewAccessibility") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Check that class includes the proper attributes in the AndroidManifest.xml + checkManifestAttributes() + + surfaceView = SurfaceView(this).apply { setContentView(this) } + wallpaperEngine = getWallpaperEngine(this, surfaceView) + wallpaperEngine.create() + surfaceView.holder.addCallback(object : SurfaceHolder.Callback { + override fun surfaceChanged( + holder: SurfaceHolder, + format: Int, + width: Int, + height: Int + ) { + wallpaperEngine.resize(width, height) + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + } + + override fun surfaceCreated(holder: SurfaceHolder) { + } + }) + + // Pass the touch events. + if (wallpaperEngine is TorusTouchListener) { + surfaceView.setOnTouchListener { _, event -> + (wallpaperEngine as TorusTouchListener).onTouchEvent(event) + true + } + } + } + + override fun onResume() { + super.onResume() + wallpaperEngine.resume() + } + + override fun onPause() { + super.onPause() + wallpaperEngine.pause() + } + + override fun onDestroy() { + super.onDestroy() + wallpaperEngine.destroy() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + if (wallpaperEngine is ConfigurationChangeListener) { + (wallpaperEngine as ConfigurationChangeListener).onConfigurationChanged(newConfig) + } + } + + private fun checkManifestAttributes() { + val configChange = packageManager.getActivityInfo(componentName, 0).configChanges + + // Check if Activity sets android:configChanges="uiMode" in the manifest. + if ((configChange and ActivityInfo.CONFIG_UI_MODE) != ActivityInfo.CONFIG_UI_MODE) { + throw RuntimeException( + "${TorusViewerActivity::class.simpleName} " + + "has to include the attribute android:configChanges=\"uiMode\" " + + "in the AndroidManifest.xml" + ) + } + } +} diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/app/KeyguardLockController.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/app/KeyguardLockController.kt new file mode 100644 index 0000000..cfa378c --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/app/KeyguardLockController.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 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.torus.core.app + +import android.app.KeyguardManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter + +/** + * Listens to keyguard lock state changes. + * + * @constructor Creates a new [KeyguardLockController]. + * @param lockStateListener a listener that we receive Keyguard Lock state changes. + */ +class KeyguardLockController( + private val context: Context, + private val lockStateListener: LockStateListener? = null +) { + @Volatile + var locked: Boolean = false + private set + + private val userPresentReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_USER_PRESENT) onChange(false) + } + } + + private val userPresentIntentFilter: IntentFilter = IntentFilter(Intent.ACTION_USER_PRESENT) + private val keyguardManager = + context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager? + private var isRegistered: Boolean = false + + init { + keyguardManager?.let { locked = it.isKeyguardLocked } + } + + /** + * Starts listening for [Intent.ACTION_USER_PRESENT] state changes. This should be used + * together with [KeyguardLockController.updateLockState] to detect lock state changes. Using a + * broadcast listener is not ideal, but there isn't an alternative event to detect lock state + * changes. + */ + fun start() { + context.registerReceiver(userPresentReceiver, userPresentIntentFilter) + isRegistered = true + } + + /** + * Stops listening for [Intent.ACTION_USER_PRESENT] state changes. This should be used + * together with [KeyguardLockController.updateLockState] to detect lock state changes. Using a + * broadcast listener is not ideal, but there isn't an alternative event to detect lock state + * changes. + */ + fun stop() { + if (isRegistered) { + context.unregisterReceiver(userPresentReceiver) + isRegistered = false + } + } + + /** + * Reads the [KeyguardManager.isKeyguardLocked] new value to know the current Lock state. + * This function should be also called on Screen state changes (i.e. [Intent.ACTION_SCREEN_ON], + * [Intent.ACTION_SCREEN_OFF]). This function can be used also to do polling of the lock state. + */ + fun updateLockState() = keyguardManager?.let { onChange(it.isKeyguardLocked) } + + private fun onChange(locked: Boolean) { + if (this.locked != locked) { + this.locked = locked + lockStateListener?.onLockStateChanged(locked) + } + } + + /** Interface to listen to Keyguard lock changes. */ + interface LockStateListener { + /** + * Called when the Keyguard lock state has changed. + * + * @param locked true if the keyguard is currently locked. + */ + fun onLockStateChanged(locked: Boolean) + } +} diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/content/ConfigurationChangeListener.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/content/ConfigurationChangeListener.kt new file mode 100644 index 0000000..e6c479a --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/content/ConfigurationChangeListener.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 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.torus.core.content + +import android.content.res.Configuration + +/** + * Interface to listen to configuration changes. + */ +interface ConfigurationChangeListener { + fun onConfigurationChanged(newConfig: Configuration) +} \ No newline at end of file diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/engine/TorusEngine.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/engine/TorusEngine.kt new file mode 100644 index 0000000..9685816 --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/engine/TorusEngine.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 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.torus.core.engine + +import com.google.android.torus.core.wallpaper.LiveWallpaper + +/** + * Interface that defines a Live Wallpaper Engine and its different states. You need to implement + * this class to render using [LiveWallpaper]. + */ +interface TorusEngine { + /** + * Called when the engine is created. You should load the assets and initialize the + * resources here. + * + * IMPORTANT: When this function is called, the surface used to render the engine has to be + * ready. + * + * @param isFirstActiveInstance Whether this is the first Engine instance (since the last time + * that all instances were destroyed). + */ + fun create(isFirstActiveInstance: Boolean = true) + + /** + * Called when the [TorusEngine] resumes. + */ + fun resume() + + /** + * Called when the [TorusEngine] is paused. + */ + fun pause() + + /** + * Called when the surface holding the [TorusEngine] has changed its size. + * + * @param width The new width of the surface holding the [TorusEngine]. + * @param height The new height of the surface holding the [TorusEngine]. + */ + fun resize(width: Int, height: Int) + + /** + * Called when we need to destroy the surface. + * + * @param isLastActiveInstance Whether this was the last Engine instance in our Service. + */ + fun destroy(isLastActiveInstance: Boolean = true) +} diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/engine/listener/TorusTouchListener.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/engine/listener/TorusTouchListener.kt new file mode 100644 index 0000000..4ceba4a --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/engine/listener/TorusTouchListener.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 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.torus.core.engine.listener + +import android.view.MotionEvent +import com.google.android.torus.core.engine.TorusEngine + +/** + * Allows to receive Touch events. + * The Interface must be implemented by a [TorusEngine] instance, + */ +interface TorusTouchListener { + /** + * Called when a touch event has been triggered. + * + * @param event The new [MotionEvent]. + */ + fun onTouchEvent(event: MotionEvent) +} \ No newline at end of file diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/extensions/ConfigurationExt.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/extensions/ConfigurationExt.kt new file mode 100644 index 0000000..2dd043b --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/extensions/ConfigurationExt.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 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.torus.core.extensions + +import android.content.res.Configuration + +/** + * Extension function that serves as a shortcut to know if we are currently in Dark Mode. + * + * @return true if we are in Dark Mode; false otherwise. + */ +fun Configuration.isDarkMode(): Boolean { + return (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES +} \ No newline at end of file diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/geometry/Vertex.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/geometry/Vertex.kt new file mode 100644 index 0000000..57453bf --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/geometry/Vertex.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 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.torus.core.geometry + +import java.nio.ByteBuffer + +/** + * Defines the information a vertex, as an array of different numbers. + */ +class Vertex(vararg input: Number) { + private val vertexValues: ArrayList = ArrayList() + + init { + for (item in input) { + vertexValues.add(item) + } + } + + /** + * Function that will help to add each vertex into a ByteBuffer. + * @param buffer The [ByteBuffer] where we are adding the current vertex. + */ + fun putInto(buffer: ByteBuffer) { + for (vertexValue in vertexValues) { + when (vertexValue) { + is Float -> buffer.putFloat(vertexValue.toFloat()) + is Int -> buffer.putInt(vertexValue.toInt()) + is Short -> buffer.putShort(vertexValue.toShort()) + } + } + } +} \ No newline at end of file diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/power/FpsThrottler.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/power/FpsThrottler.kt new file mode 100644 index 0000000..873327d --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/power/FpsThrottler.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2023 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.torus.core.power + +/** + * This class determines ready-to-render conditions for the engine's main loop in order to target a + * requested frame rate. + */ +class FpsThrottler { + companion object { + private const val NANO_TO_MILLIS = 1 / 1E6 + + const val FPS_120 = 120f + const val FPS_60 = 60f + const val FPS_30 = 30f + const val FPS_18 = 18f + + @Deprecated(message = "Use FPS_60 instead.") + const val HIGH_FPS = 60f + @Deprecated(message = "Use FPS_30 instead.") + const val MED_FPS = 30f + @Deprecated(message = "Use FPS_18 instead.") + const val LOW_FPS = 18f + } + + private var fps: Float = FPS_60 + + @Volatile + private var frameTimeMillis: Double = 1000.0 / fps.toDouble() + private var lastFrameTimeNanos: Long = -1 + + @Volatile + private var continuousRenderingMode: Boolean = true + + @Volatile + private var requestRendering: Boolean = false + + private fun updateFrameTime() { + frameTimeMillis = 1000.0 / fps.toDouble() + } + + /** + * If [fps] is non-zero, update the requested FPS and calculate the frame time + * for the requested FPS. Otherwise disable continuous rendering (on demand rendering) + * without changing the frame rate. + * + * @param fps The requested FPS value. + */ + fun updateFps(fps: Float) { + if (fps <= 0f) { + setContinuousRenderingMode(false) + } else { + setContinuousRenderingMode(true) + this.fps = fps + updateFrameTime() + } + } + + /** + * Sets rendering mode to continuous or on demand. + * + * @param continuousRenderingMode When true enable continuous rendering. When false disable + * continuous rendering (on demand). + */ + fun setContinuousRenderingMode(continuousRenderingMode: Boolean) { + this.continuousRenderingMode = continuousRenderingMode + } + + /** Request a new render frame (in on demand rendering mode). */ + fun requestRendering() { + requestRendering = true + } + + /** + * Calculates whether we can render the next frame. In continuous mode return true only + * if enough time has passed since the last render to maintain requested FPS. + * In on demand mode, return true only if [requestRendering] was called to render + * the next frame. + * + * @param frameTimeNanos The time in nanoseconds when the current frame started. + * + * @return true if we can render the next frame. + */ + fun canRender(frameTimeNanos: Long): Boolean { + return if (continuousRenderingMode) { + // continuous rendering + if (lastFrameTimeNanos == -1L) { + true + } else { + val deltaMillis = (frameTimeNanos - lastFrameTimeNanos) * NANO_TO_MILLIS + return (deltaMillis >= frameTimeMillis) && (fps > 0f) + } + } else { + // on demand rendering + requestRendering + } + } + + /** + * Attempt to render a frame, if throttling permits it at this time. The delegate + * [onRenderPermitted] will be called to handle the rendering if so. The delegate may decide to + * skip the frame for any other reason, and then should return false. If the frame is actually + * rendered, the delegate must return true to ensure that the next frame will be scheduled for + * the correct time. + * + * @param frameTimeNanos The time in nanoseconds when the current frame started. + * @param onRenderPermitted The client delegate to dispatch if rendering is permitted at this + * time. + * + * @return true if a frame is permitted and then actually rendered. + */ + fun tryRender(frameTimeNanos: Long, onRenderPermitted: () -> Boolean): Boolean { + if (canRender(frameTimeNanos) && onRenderPermitted()) { + // For pacing, record the time when the frame *started*, not when it finished rendering. + lastFrameTimeNanos = frameTimeNanos + requestRendering = false + return true + } + return false + } +} diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/time/TimeController.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/time/TimeController.kt new file mode 100644 index 0000000..c8647f8 --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/time/TimeController.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 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.torus.core.time + +import android.os.SystemClock + +/** + * Class in charge of controlling the delta time and the elapsed time. This will help with a + * common scenario in any Computer Graphics engine. + */ +class TimeController { + companion object { + private val MIN_THRESHOLD_OVERFLOW = Float.MAX_VALUE / 1E20f + private const val MILLIS_TO_SECONDS = 1 / 1000f + } + + /** + * The elapsed time, since the last time it was reset, in seconds. + */ + var elapsedTime = 0f + private set + + /** + * The delta time from the last frame, in milliseconds. + */ + var deltaTimeMillis: Long = 0 + private set + + private var lastTimeMillis: Long = 0 + + init { + resetDeltaTime() + } + + /** + * Resets the delta time and last time since previous frame, and sets last time to + * [currentTimeMillis] and increases the elapsed time. + * + * @property currentTimeMillis The last known frame time, in milliseconds. + */ + fun resetDeltaTime(currentTimeMillis: Long = SystemClock.elapsedRealtime()) { + lastTimeMillis = currentTimeMillis + elapsedTime += deltaTimeMillis * MILLIS_TO_SECONDS + deltaTimeMillis = 0 + } + + /** + * Resets elapse time in case is bigger than the max value (to avoid overflow) + */ + fun resetElapsedTimeIfNeeded() { + if (elapsedTime > MIN_THRESHOLD_OVERFLOW) { + elapsedTime = 0f + } + } + + /** + * Calculates the delta time (in milliseconds) based on the current time + * and the last saved time. + * + * @property currentTimeMillis The last known frame time, in milliseconds. + */ + fun updateDeltaTime(currentTimeMillis: Long = SystemClock.elapsedRealtime()) { + deltaTimeMillis = currentTimeMillis - lastTimeMillis + } +} \ No newline at end of file diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt new file mode 100644 index 0000000..c69bff8 --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2023 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.torus.core.wallpaper + +import android.app.WallpaperColors +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.res.Configuration +import android.graphics.PixelFormat +import android.os.Build +import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE +import android.os.Bundle +import android.service.wallpaper.WallpaperService +import android.view.MotionEvent +import android.view.SurfaceHolder +import androidx.annotation.RequiresApi +import com.google.android.torus.core.content.ConfigurationChangeListener +import com.google.android.torus.core.engine.TorusEngine +import com.google.android.torus.core.engine.listener.TorusTouchListener +import com.google.android.torus.core.wallpaper.listener.LiveWallpaperEventListener +import com.google.android.torus.core.wallpaper.listener.LiveWallpaperKeyguardEventListener +import java.lang.ref.WeakReference + +/** + * Implements [WallpaperService] using Filament to render the wallpaper. + * An instance of this class should only implement [getWallpaperEngine] + * + * Note: [LiveWallpaper] subclasses must include the following attribute/s + * in the AndroidManifest.xml: + * - android:configChanges="uiMode" + */ +abstract class LiveWallpaper : WallpaperService() { + private companion object { + const val COMMAND_REAPPLY = "android.wallpaper.reapply" + const val COMMAND_WAKING_UP = "android.wallpaper.wakingup" + const val COMMAND_KEYGUARD_GOING_AWAY = "android.wallpaper.keyguardgoingaway" + const val COMMAND_GOING_TO_SLEEP = "android.wallpaper.goingtosleep" + const val COMMAND_PREVIEW_INFO = "android.wallpaper.previewinfo" + const val WALLPAPER_FLAG_NOT_FOUND = -1 + } + + // Holds the number of concurrent engines. + private var numEngines = 0 + + // We can have multiple ConfigurationChangeListener because we can have multiple engines. + private val configChangeListeners: ArrayList> = + ArrayList() + + // This is only needed for <= android R. + private val wakeStateChangeListeners: ArrayList> = + ArrayList() + private lateinit var wakeStateReceiver: BroadcastReceiver + + override fun onCreate() { + super.onCreate() + + val wakeStateChangeIntentFilter = IntentFilter() + wakeStateChangeIntentFilter.addAction(Intent.ACTION_SCREEN_ON) + wakeStateChangeIntentFilter.addAction(Intent.ACTION_SCREEN_OFF) + + /* + * Only For Android R (SDK 30) or lower. Starting from S we can get wake/sleep events + * through WallpaperService.Engine.onCommand events that should be more accurate. + */ + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + wakeStateReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val positionExtras = Bundle() + when (intent.action) { + Intent.ACTION_SCREEN_ON -> { + positionExtras.putInt( + LiveWallpaperEventListener.WAKE_ACTION_LOCATION_X, + -1 + ) + positionExtras.putInt( + LiveWallpaperEventListener.WAKE_ACTION_LOCATION_Y, + -1 + ) + wakeStateChangeListeners.forEach { + it.get()?.onWake(positionExtras) + } + } + + Intent.ACTION_SCREEN_OFF -> { + positionExtras.putInt( + LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_X, + -1 + ) + positionExtras.putInt( + LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_Y, + -1 + ) + wakeStateChangeListeners.forEach { + it.get()?.onSleep(positionExtras) + } + } + } + } + } + registerReceiver(wakeStateReceiver, wakeStateChangeIntentFilter) + } + } + + override fun onDestroy() { + super.onDestroy() + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) unregisterReceiver(wakeStateReceiver) + } + + /** + * Must be implemented to return a new instance of [TorusEngine]. + * If you want it to subscribe to wallpaper interactions (offset, preview, zoom...) the engine + * should also implement [LiveWallpaperEventListener]. If you want it to subscribe to touch + * events, it should implement [TorusTouchListener]. + * + * Note: You might have multiple Engines running at the same time (when the wallpaper is set as + * the active wallpaper and the user is in the wallpaper picker viewing a preview of it + * as well). You can track the lifecycle when *any* Engine is active using the + * is{First/Last}ActiveInstance parameters of the create/destroy methods. + * + */ + abstract fun getWallpaperEngine(context: Context, surfaceHolder: SurfaceHolder): TorusEngine + + /** + * returns a new instance of [LiveWallpaperEngineWrapper]. + * Caution: This function should not be override when extending [LiveWallpaper] class. + */ + override fun onCreateEngine(): Engine { + val wrapper = LiveWallpaperEngineWrapper() + wakeStateChangeListeners.add(WeakReference(wrapper)) + return wrapper + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + for (reference in configChangeListeners) { + reference.get()?.onConfigurationChanged(newConfig) + } + } + + private fun addConfigChangeListener(configChangeListener: ConfigurationChangeListener) { + var containsListener = false + + for (reference in configChangeListeners) { + if (configChangeListener == reference.get()) { + containsListener = true + break + } + } + + if (!containsListener) { + configChangeListeners.add(WeakReference(configChangeListener)) + } + } + + private fun removeConfigChangeListener(configChangeListener: ConfigurationChangeListener) { + for (reference in configChangeListeners) { + if (configChangeListener == reference.get()) { + configChangeListeners.remove(reference) + break + } + } + } + + /** + * Class that enables to connect a [TorusEngine] with some [WallpaperService.Engine] functions. + * The class that you use to render in a [LiveWallpaper] needs to inherit from + * [LiveWallpaperConnector] and implement [TorusEngine]. + */ + open class LiveWallpaperConnector { + private var wallpaperServiceEngine: WallpaperService.Engine? = null + + /** + * Returns the information if the wallpaper is in preview mode. This value doesn't change + * during a [TorusEngine] lifecycle, so you can know if the wallpaper is set checking that + * on create isPreview == false. + */ + fun isPreview(): Boolean { + this.wallpaperServiceEngine?.let { + return it.isPreview + } + return false + } + + /** + * Triggers the [WallpaperService] to recompute the Wallpaper Colors. + */ + fun notifyWallpaperColorsChanged() { + this.wallpaperServiceEngine?.notifyColorsChanged() + } + + /** Returns the current Engine [SurfaceHolder]. */ + fun getEngineSurfaceHolder(): SurfaceHolder? = this.wallpaperServiceEngine?.surfaceHolder + + /** Returns the wallpaper flags indicating which screen this Engine is rendering to. */ + @RequiresApi(UPSIDE_DOWN_CAKE) + fun getWallpaperFlags(): Int { + this.wallpaperServiceEngine?.let { + return it.wallpaperFlags + } + return WALLPAPER_FLAG_NOT_FOUND + } + + internal fun setServiceEngineReference(wallpaperServiceEngine: WallpaperService.Engine) { + this.wallpaperServiceEngine = wallpaperServiceEngine + } + } + + /** + * Implementation of [WallpaperService.Engine] that works as a wrapper. If we used a + * [WallpaperService.Engine] instance as the framework engine, we would find the problem + * that the engine will be created for preview, then destroyed and recreated again when the + * wallpaper is set. This behavior may cause to load assets multiple time for every time the + * Rendering engine is created. Also, wrapping our [TorusEngine] inside + * [WallpaperService.Engine] allow us to reuse [TorusEngine] in other places, like Activities. + */ + private inner class LiveWallpaperEngineWrapper : WallpaperService.Engine() { + private lateinit var wallpaperEngine: TorusEngine + + override fun onCreate(surfaceHolder: SurfaceHolder) { + super.onCreate(surfaceHolder) + // Use RGBA_8888 format. + surfaceHolder.setFormat(PixelFormat.RGBA_8888) + + /* + * For Android 10 (SDK 29). + * This is needed for Foldables and multiple display devices. + */ + val context = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + displayContext ?: this@LiveWallpaper + } else { + this@LiveWallpaper + } + + wallpaperEngine = getWallpaperEngine(context, surfaceHolder) + numEngines++ + + /* + * It is important to call setTouchEventsEnabled in onCreate for it to work. Calling it + * in onSurfaceCreated instead will cause the engine to be stuck in an instantiation + * loop. + */ + if (wallpaperEngine is TorusTouchListener) setTouchEventsEnabled(true) + } + + override fun onSurfaceCreated(holder: SurfaceHolder) { + super.onSurfaceCreated(holder) + + if (wallpaperEngine is ConfigurationChangeListener) { + addConfigChangeListener(wallpaperEngine as ConfigurationChangeListener) + } + + if (wallpaperEngine is LiveWallpaperConnector) { + (wallpaperEngine as LiveWallpaperConnector).setServiceEngineReference(this) + } + + wallpaperEngine.create(numEngines == 1) + } + + override fun onSurfaceDestroyed(holder: SurfaceHolder?) { + super.onSurfaceDestroyed(holder) + numEngines-- + + if (wallpaperEngine is ConfigurationChangeListener) { + removeConfigChangeListener(wallpaperEngine as ConfigurationChangeListener) + } + + var isLastInstance = false + if (numEngines <= 0) { + numEngines = 0 + isLastInstance = true + } + + if (isVisible) wallpaperEngine.pause() + wallpaperEngine.destroy(isLastInstance) + } + + override fun onSurfaceChanged( + holder: SurfaceHolder?, + format: Int, + width: Int, + height: Int + ) { + super.onSurfaceChanged(holder, format, width, height) + wallpaperEngine.resize(width, height) + } + + override fun onOffsetsChanged( + xOffset: Float, + yOffset: Float, + xOffsetStep: Float, + yOffsetStep: Float, + xPixelOffset: Int, + yPixelOffset: Int + ) { + super.onOffsetsChanged( + xOffset, + yOffset, + xOffsetStep, + yOffsetStep, + xPixelOffset, + yPixelOffset + ) + + if (wallpaperEngine is LiveWallpaperEventListener) { + (wallpaperEngine as LiveWallpaperEventListener).onOffsetChanged( + xOffset, + if (xOffsetStep.compareTo(0f) == 0) { + 1.0f + } else { + xOffsetStep + } + ) + } + } + + override fun onZoomChanged(zoom: Float) { + super.onZoomChanged(zoom) + if (wallpaperEngine is LiveWallpaperEventListener) { + (wallpaperEngine as LiveWallpaperEventListener).onZoomChanged(zoom) + } + } + + override fun onVisibilityChanged(visible: Boolean) { + super.onVisibilityChanged(visible) + if (visible) { + wallpaperEngine.resume() + } else { + wallpaperEngine.pause() + } + } + + override fun onComputeColors(): WallpaperColors? { + if (wallpaperEngine is LiveWallpaperEventListener) { + val colors = + (wallpaperEngine as LiveWallpaperEventListener).computeWallpaperColors() + + if (colors != null) { + return colors + } + } + + return super.onComputeColors() + } + + override fun onCommand( + action: String?, + x: Int, + y: Int, + z: Int, + extras: Bundle?, + resultRequested: Boolean + ): Bundle? { + when (action) { + COMMAND_REAPPLY -> onWallpaperReapplied() + COMMAND_WAKING_UP -> { + val positionExtras = extras ?: Bundle() + positionExtras.putInt(LiveWallpaperEventListener.WAKE_ACTION_LOCATION_X, x) + positionExtras.putInt(LiveWallpaperEventListener.WAKE_ACTION_LOCATION_Y, y) + onWake(positionExtras) + } + COMMAND_GOING_TO_SLEEP -> { + val positionExtras = extras ?: Bundle() + positionExtras.putInt(LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_X, x) + positionExtras.putInt(LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_Y, y) + onSleep(positionExtras) + } + COMMAND_KEYGUARD_GOING_AWAY -> onKeyguardGoingAway() + COMMAND_PREVIEW_INFO -> onPreviewInfoReceived(extras) + } + + if (resultRequested) return extras + + return super.onCommand(action, x, y, z, extras, resultRequested) + } + + override fun onTouchEvent(event: MotionEvent) { + super.onTouchEvent(event) + + if (wallpaperEngine is TorusTouchListener) { + (wallpaperEngine as TorusTouchListener).onTouchEvent(event) + } + } + + /** + * This is overriding a hidden API [WallpaperService.shouldZoomOutWallpaper]. + */ + fun shouldZoomOutWallpaper(): Boolean { + if (wallpaperEngine is LiveWallpaperEventListener) { + return (wallpaperEngine as LiveWallpaperEventListener).shouldZoomOutWallpaper() + } + return false + } + + fun onWake(extras: Bundle) { + if (wallpaperEngine is LiveWallpaperEventListener) { + (wallpaperEngine as LiveWallpaperEventListener).onWake(extras) + } + } + + fun onSleep(extras: Bundle) { + if (wallpaperEngine is LiveWallpaperEventListener) { + (wallpaperEngine as LiveWallpaperEventListener).onSleep(extras) + } + } + + fun onWallpaperReapplied() { + if (wallpaperEngine is LiveWallpaperEventListener) { + (wallpaperEngine as LiveWallpaperEventListener).onWallpaperReapplied() + } + } + + fun onKeyguardGoingAway() { + if (wallpaperEngine is LiveWallpaperKeyguardEventListener) { + (wallpaperEngine as LiveWallpaperKeyguardEventListener).onKeyguardGoingAway() + } + } + + fun onPreviewInfoReceived(extras: Bundle?) { + if (wallpaperEngine is LiveWallpaperEventListener) { + (wallpaperEngine as LiveWallpaperEventListener).onPreviewInfoReceived(extras) + } + } + } +} diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/listener/LiveWallpaperEventListener.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/listener/LiveWallpaperEventListener.kt new file mode 100644 index 0000000..6803bd0 --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/listener/LiveWallpaperEventListener.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 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.torus.core.wallpaper.listener + +import android.app.WallpaperColors +import android.os.Bundle + +/** + * Interface that is used to implement specific wallpaper callbacks like offset change (user swipes + * between home pages), when the preview state has changed or when the zoom state has changed. + */ +interface LiveWallpaperEventListener { + companion object { + const val WAKE_ACTION_LOCATION_X: String = "WAKE_ACTION_LOCATION_X" + const val WAKE_ACTION_LOCATION_Y: String = "WAKE_ACTION_LOCATION_Y" + const val SLEEP_ACTION_LOCATION_X: String = "SLEEP_ACTION_LOCATION_X" + const val SLEEP_ACTION_LOCATION_Y: String = "SLEEP_ACTION_LOCATION_Y" + } + + /** + * Called when the wallpaper has been scrolled (usually when the user scroll between pages in + * the home of the launcher). This only tracts the horizontal scroll. + * + * @param xOffset The current offset of the scroll. The value is normalize between [0,1]. + * @param xOffsetStep How is stepped the scroll. If you invert [xOffsetStep] you get the + * number of pages in the scrolling area. + */ + fun onOffsetChanged(xOffset: Float, xOffsetStep: Float) + + /** + * Called when the zoom level of the wallpaper is changing. + * + * @param zoomLevel A value between 0 and 1 that tells how much the wallpaper should be zoomed + * out: if 0, the wallpaper should be in normal state; if 1 the wallpaper should be zoomed out. + */ + fun onZoomChanged(zoomLevel: Float) + + /** + * Call when the wallpaper was set, and then is reapplied. This means that the wallpaper was + * set and is being set again. This is useful to know if the wallpaper settings have to be + * reapplied again (i.e. if the user enters the wallpaper picker and picks the same wallpaper, + * changes the settings and sets the wallpaper again). + */ + fun onWallpaperReapplied() + + /** + * Called when the Wallpaper colors need to be computed you can create a [WallpaperColors] + * instance using the [WallpaperColors.fromBitmap] function and passing a bitmap that + * represents the wallpaper (i.e. the gallery thumbnail) or use the [WallpaperColors] + * constructor and pass the primary, secondary and tertiary colors. This method is specially + * important since the UI will change their colors based on what is returned here. + * + * @return The colors that represent the wallpaper; null if you want the System to take + * care of the colors. + */ + fun computeWallpaperColors(): WallpaperColors? + + /** + * Called when the wallpaper receives the preview information (asynchronous call). + * + * @param extras the bundle of the preview information. The key "which_preview" can be used to + * retrieve a string value (ex. main_preview_home) that specifies which preview the engine + * is referring to. + */ + fun onPreviewInfoReceived(extras: Bundle?) {} + + /** + * Called when the device is activated from a sleep/AOD state. + * + * @param extras contains the location of the action that caused the wake event: + * - [LiveWallpaperEventListener.WAKE_ACTION_LOCATION_X]: the X screen location (in Pixels). if + * the value is not included or is -1, the X screen location is unknown. + * - [LiveWallpaperEventListener.WAKE_ACTION_LOCATION_Y]: the Y screen location (in Pixels). if + * the value is not included or is -1, the Y screen location is unknown. + */ + fun onWake(extras: Bundle) + + /** + * Called when the device enters a sleep/AOD state. + * + * @param extras contains the location of the action that caused the sleep event: + * - [LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_X]: the X screen location (in Pixels). if + * the value is not included or is -1, the X screen location is unknown. + * - [LiveWallpaperEventListener.SLEEP_ACTION_LOCATION_Y]: the Y screen location (in Pixels). if + * the value is not included or is -1, the Y screen location is unknown. + */ + fun onSleep(extras: Bundle) + + /** + * Indicates whether the zoom animation should be handled in WindowManager. Preferred to be set + * to true to avoid pressuring GPU. + * + * See [WallpaperService.shouldZoomOutWallpaper]. + */ + fun shouldZoomOutWallpaper() = false +} diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/listener/LiveWallpaperKeyguardEventListener.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/listener/LiveWallpaperKeyguardEventListener.kt new file mode 100644 index 0000000..70b15e5 --- /dev/null +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/listener/LiveWallpaperKeyguardEventListener.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 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.torus.core.wallpaper.listener + +/** Interface that is used to implement specific wallpaper callbacks related to keyguard events. */ +interface LiveWallpaperKeyguardEventListener { + + /** Called when the keyguard is going away. */ + fun onKeyguardGoingAway() +} diff --git a/toruslib/torus-framework-canvas/build.gradle b/toruslib/torus-framework-canvas/build.gradle new file mode 100644 index 0000000..8d59718 --- /dev/null +++ b/toruslib/torus-framework-canvas/build.gradle @@ -0,0 +1,18 @@ +// Copyright (C) 2023 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. + +dependencies { + implementation project(':torus-core') + implementation project(':torus-utils') +} diff --git a/toruslib/torus-framework-canvas/src/main/AndroidManifest.xml b/toruslib/torus-framework-canvas/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3c68a96 --- /dev/null +++ b/toruslib/torus-framework-canvas/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + diff --git a/toruslib/torus-framework-canvas/src/main/java/com/google/android/torus/canvas/engine/CanvasWallpaperEngine.kt b/toruslib/torus-framework-canvas/src/main/java/com/google/android/torus/canvas/engine/CanvasWallpaperEngine.kt new file mode 100644 index 0000000..814dff6 --- /dev/null +++ b/toruslib/torus-framework-canvas/src/main/java/com/google/android/torus/canvas/engine/CanvasWallpaperEngine.kt @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2022 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.torus.canvas.engine + +import android.graphics.Canvas +import android.graphics.RuntimeShader +import android.os.SystemClock +import android.util.Log +import android.util.Size +import android.view.Choreographer +import android.view.SurfaceHolder +import androidx.annotation.VisibleForTesting +import com.google.android.torus.core.engine.TorusEngine +import com.google.android.torus.core.power.FpsThrottler +import com.google.android.torus.core.time.TimeController +import com.google.android.torus.core.wallpaper.LiveWallpaper +import java.io.PrintWriter + +/** + * Class that implements [TorusEngine] using Canvas and can be used in a [LiveWallpaper]. This + * class also inherits from [LiveWallpaper.LiveWallpaperConnector] which allows to do some calls + * related to Live Wallpapers, like the method [isPreview] or [notifyWallpaperColorsChanged]. + * + * By default it won't start [startUpdateLoop]. To run animations and update logic per frame, call + * [startUpdateLoop] and [stopUpdateLoop] when it's no longer needed. + * + * This class also can be used with the new RuntimeShader. + */ +abstract class CanvasWallpaperEngine( + /** The default [SurfaceHolder] to be used. */ + private val defaultHolder: SurfaceHolder, + + /** + * Defines if the surface should be hardware accelerated or not. If you are using + * [RuntimeShader], this value should be set to true. When setting it to true, some + * functions might not be supported. Please refer to the documentation: + * https://developer.android.com/guide/topics/graphics/hardware-accel#unsupported + */ + private val hardwareAccelerated: Boolean = false, +) : LiveWallpaper.LiveWallpaperConnector(), TorusEngine { + + private val choreographer = Choreographer.getInstance() + private val timeController = TimeController().also { + it.resetDeltaTime(SystemClock.uptimeMillis()) + } + private val frameScheduler = FrameCallback() + private val fpsThrottler = FpsThrottler() + + protected var screenSize = Size(0, 0) + private set + private var resizeCalled: Boolean = false + + private var isWallpaperEngineVisible = false + /** + * Indicates whether the engine#onCreate is called. + * + * TODO(b/277672928): These two booleans were introduced as a workaround where + * [onSurfaceRedrawNeeded] called after an [onSurfaceDestroyed], without [onCreate]/ + * [onSurfaceCreated] being called between those. Remove these once it's fixed in + * [WallpaperService]. + */ + private var isCreated = false + private var shouldInvokeResume = false + + /** Callback to handle when the [TorusEngine] has been created. */ + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + open fun onCreate(isFirstActiveInstance: Boolean) { + // No-op. Ready for being overridden by children. + } + + /** Callback to handle when the [TorusEngine] has been resumed. */ + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + open fun onResume() { + // No-op. Ready for being overridden by children. + } + + /** Callback to handle when the [TorusEngine] has been paused. */ + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + open fun onPause() { + // No-op. Ready for being overridden by children. + } + + /** + * Callback to handle when the surface holding the [TorusEngine] has changed its size. + * + * @param size The new size of the surface holding the [TorusEngine]. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + open fun onResize(size: Size) { + // No-op. Ready for being overridden by children. + } + + /** + * Callback to handle when the [TorusEngine] needs to be updated. Call [startUpdateLoop] to + * initiate the frame loop; call [stopUpdateLoop] to end the loop. The client is supposed to + * update logic and render in this loop. + * + * @param deltaMillis The time in millis since the last time [onUpdate] was called. + * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, + * in the [System.nanoTime] timebase. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + open fun onUpdate(deltaMillis: Long, frameTimeNanos: Long) { + // No-op. Ready for being overridden by children. + } + + /** + * Callback to handle when we need to destroy the surface. + * + * @param isLastActiveInstance Whether this was the last wallpaper engine instance (until the + * next [onCreate]). + */ + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + open fun onDestroy(isLastActiveInstance: Boolean) { + // No-op. Ready for being overridden by children. + } + + final override fun create(isFirstActiveInstance: Boolean) { + screenSize = Size( + getCurrentSurfaceHolder().surfaceFrame.width(), + getCurrentSurfaceHolder().surfaceFrame.height() + ) + + onCreate(isFirstActiveInstance) + + isCreated = true + + if (shouldInvokeResume) { + Log.e( + TAG, "Force invoke resume. onVisibilityChanged must have been called" + + "before onCreate.") + resume() + shouldInvokeResume = false + } + } + + final override fun pause() { + if (!isCreated) { + Log.e( + TAG, "Engine is not yet created but pause is called. Set a flag to invoke" + + " resume on next create.") + shouldInvokeResume = true + return + } + + if (isWallpaperEngineVisible) { + onPause() + isWallpaperEngineVisible = false + } + } + + final override fun resume() { + if (!isCreated) { + Log.e( + TAG, "Engine is not yet created but resume is called. Set a flag to " + + "invoke resume on next create.") + shouldInvokeResume = true + return + } + + if (!isWallpaperEngineVisible) { + onResume() + isWallpaperEngineVisible = true + } + } + + final override fun resize(width: Int, height: Int) { + resizeCalled = true + + screenSize = Size(width, height) + onResize(screenSize) + } + + final override fun destroy(isLastActiveInstance: Boolean) { + choreographer.removeFrameCallback(frameScheduler) + timeController.resetDeltaTime(SystemClock.uptimeMillis()) + + // Always detach the surface before destroying the engine + onDestroy(isLastActiveInstance) + } + + /** + * Renders to canvas. Use this in [onUpdate] loop. This will automatically throttle (or limit) + * FPS that was set via [setFpsLimit]. + * + * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, in the + * [System.nanoTime] timebase. + * @param onRender The callback triggered when the canvas is ready for render. + * + * @return Whether it is rendered. + */ + fun renderWithFpsLimit(frameTimeNanos: Long, onRender: (canvas: Canvas) -> Unit): Boolean { + if (resizeCalled) { + /** + * Skip rendering a frame to a buffer with potentially-outdated dimensions, and request + * redraw in the next frame. + */ + resizeCalled = false + + fpsThrottler.requestRendering() + return renderWithFpsLimit(frameTimeNanos, onRender) + } + + return fpsThrottler.tryRender(frameTimeNanos) { + renderToCanvas(onRender) + } + } + + /** + * Renders to canvas. + * + * @param onRender The callback triggered when the canvas is ready for render. + * + * @return Whether it is rendered. + */ + fun render(onRender: (canvas: Canvas) -> Unit): Boolean { + if (resizeCalled) { + /** + * Skip rendering a frame to a buffer with potentially-outdated dimensions, and request + * redraw in the next frame. + */ + resizeCalled = false + return render(onRender) + } + + return renderToCanvas(onRender) + } + + /** + * Sets the FPS limit. See [FpsThrottler] for the FPS constants. The max FPS will be the screen + * refresh (VSYNC) rate. + * + * @param fps Desired mas FPS. + */ + protected fun setFpsLimit(fps: Float) { + fpsThrottler.updateFps(fps) + } + + /** + * Starts the update loop. + */ + protected fun startUpdateLoop() { + if (!frameScheduler.running) { + frameScheduler.running = true + choreographer.postFrameCallback(frameScheduler) + } + } + + /** + * Stops the update loop. + */ + protected fun stopUpdateLoop() { + if (frameScheduler.running) { + frameScheduler.running = false + choreographer.removeFrameCallback(frameScheduler) + } + } + + private fun renderToCanvas(onRender: (canvas: Canvas) -> Unit): Boolean { + val surfaceHolder = getCurrentSurfaceHolder() + if (!surfaceHolder.surface.isValid) return false + var canvas: Canvas? = null + + try { + canvas = if (hardwareAccelerated) { + surfaceHolder.lockHardwareCanvas() + } else { + surfaceHolder.lockCanvas() + } ?: return false + + onRender(canvas) + + } catch (e: java.lang.Exception) { + Log.e("canvas_exception", "canvas exception", e) + } finally { + if (canvas != null) { + surfaceHolder.unlockCanvasAndPost(canvas) + } + } + return true + } + + private fun getCurrentSurfaceHolder(): SurfaceHolder = + getEngineSurfaceHolder() ?: defaultHolder + + /** + * Implementation of [Choreographer.FrameCallback] which triggers [onUpdate]. + */ + inner class FrameCallback : Choreographer.FrameCallback { + internal var running: Boolean = false + + override fun doFrame(frameTimeNanos: Long) { + if (running) choreographer.postFrameCallback(this) + // onUpdate should be called for every V_SYNC. + val frameTimeMillis = frameTimeNanos / 1000_000 + timeController.updateDeltaTime(frameTimeMillis) + onUpdate(timeController.deltaTimeMillis, frameTimeNanos) + timeController.resetDeltaTime(frameTimeMillis) + } + } + + /** + * Override this for dumpsys. + * + * You still need to have your WallpaperService overriding [dump] and call + * [CanvasWallpaperEngine.dump]. + * + * Usage: adb shell dumpsys activity service ${your_wallpaper_service_name}. + */ + open fun dump(out: PrintWriter) = Unit + + private companion object { + private val TAG: String = CanvasWallpaperEngine::class.java.simpleName + } +} \ No newline at end of file diff --git a/toruslib/torus-math/build.gradle b/toruslib/torus-math/build.gradle new file mode 100644 index 0000000..7dcad1d --- /dev/null +++ b/toruslib/torus-math/build.gradle @@ -0,0 +1,13 @@ +// Copyright (C) 2023 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. diff --git a/toruslib/torus-math/src/main/AndroidManifest.xml b/toruslib/torus-math/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3c68a96 --- /dev/null +++ b/toruslib/torus-math/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + diff --git a/toruslib/torus-math/src/main/java/com/google/android/torus/math/AffineTransform.kt b/toruslib/torus-math/src/main/java/com/google/android/torus/math/AffineTransform.kt new file mode 100644 index 0000000..627a6ab --- /dev/null +++ b/toruslib/torus-math/src/main/java/com/google/android/torus/math/AffineTransform.kt @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2023 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.torus.math + +import android.opengl.Matrix +import java.util.* + +/** An immutable 3D transformation in homogeneous coordinates. */ +open class AffineTransform @JvmOverloads constructor( + /** The position of the transform. */ + val position: Vector3 = Vector3(0f, 0f, 0f), + /** The rotation of the transform. */ + val rotation: RotationQuaternion = RotationQuaternion(), + /** The scale of the transform. */ + val scale: Vector3 = Vector3(1f, 1f, 1f) +) : MatrixTransform { + constructor(transform: AffineTransform) : this( + transform.position, + transform.rotation, + transform.scale + ) + + /** + * Creates a new [AffineTransform] with ([x], [y], [z]) as the new translation. + * + * @param x The X component of the translation. + * @param y The Y component of the translation. + * @param z The Z component of the translation. + * + * @return The new [AffineTransform] with the new translation. + */ + fun withTranslation(x: Float, y: Float, z: Float): AffineTransform { + return AffineTransform(Vector3(x, y, z), rotation, scale) + } + + /** + * Creates a new [AffineTransform] with ([x], [y], [z]) as the new scale. + * + * @param x The scale in the X direction. + * @param y The scale in the Y direction. + * @param z The scale in the Z direction. + * + * @return The new [AffineTransform] with the new scale. + */ + fun withScale(x: Float, y: Float, z: Float): AffineTransform { + return AffineTransform(position, rotation, Vector3(x, y, z)) + } + + /** + * Creates a new [AffineTransform] with ([scale], [scale], [scale]) as the new scale. + * + * @param scale The scale applied in the X,Y and Z directions. + * + * @return The new [AffineTransform] with the new scale. + */ + fun withScale(scale: Float): AffineTransform { + return withScale(scale, scale, scale) + } + + /** + * Creates a new [AffineTransform] with [rotation] as the new rotation. + * + * @param rotation The new rotation. + * + * @return The new [AffineTransform] with the new rotation. + */ + fun withRotation(rotation: RotationQuaternion): AffineTransform { + return AffineTransform(position, RotationQuaternion(rotation), scale) + } + + /** + * Returns a new transform with a new rotation using Euler rotation angles (ZYX sequence). + * + * @param x The Euler rotation angle around X axis, in degrees. + * @param y The Euler rotation angle around Y axis, in degrees. + * @param z The Euler rotation angle around Z axis, in degrees. + */ + fun withEulerRotation(x: Float, y: Float, z: Float): AffineTransform { + return AffineTransform(position, RotationQuaternion.fromEuler(x, y, z), scale) + } + + fun translateBy(x: Float, y: Float, z: Float): AffineTransform { + return AffineTransform(position + Vector3(x, y, z), rotation, scale) + } + + fun scaleBy(scale: Float): AffineTransform { + return AffineTransform(position, rotation, this.scale + Vector3(scale)) + } + + fun scaleBy(x: Float, y: Float, z: Float): AffineTransform { + return AffineTransform(position, rotation, this.scale + Vector3(x, y, z)) + } + + fun rotateBy(quaternion: RotationQuaternion): AffineTransform { + return AffineTransform(position, quaternion * rotation, scale) + } + + /** + * Rotates the current rotation using some Euler rotation angles (ZYX sequence). + * + * @param x The Euler rotation angle around X axis, in degrees. + * @param y The Euler rotation angle around Y axis, in degrees. + * @param z The Euler rotation angle around Z axis, in degrees. + */ + fun rotateByEuler(x: Float, y: Float, z: Float): AffineTransform { + return rotateBy(RotationQuaternion.fromEuler(x, y, z)) + } + + /** + * Returns a 4x4 transform Matrix. The format of the matrix follows the OpenGL ES matrix format + * stored in float arrays. + * Matrices are 4 x 4 column-vector matrices stored in column-major order: + * + *
+     *  m[0] m[4] m[8]  m[12]
+     *  m[1] m[5] m[9]  m[13]
+     *  m[2] m[6] m[10] m[14]
+     *  m[3] m[7] m[11] m[15]
+     *  
+ * + * @return a 16 value [FloatArray] representing the transform as a 4x4 matrix. + */ + override fun toMatrix(): FloatArray { + val transformMatrix = FloatArray(16) + Matrix.setIdentityM(transformMatrix, 0) + // The order of operations matter; we should follow the usual: Scale, Rotate and Translate. + Matrix.scaleM(transformMatrix, 0, scale.x, scale.y, scale.z) + Matrix.rotateM( + transformMatrix, + 0, + rotation.angle.toFloat(), + rotation.direction.x, + rotation.direction.y, + rotation.direction.z + ) + Matrix.translateM(transformMatrix, 0, position.x, position.y, position.z) + return transformMatrix + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AffineTransform + + if (position != other.position) return false + if (rotation != other.rotation) return false + if (scale != other.scale) return false + + return true + } + + override fun hashCode(): Int { + var result = position.hashCode() + result = 31 * result + rotation.hashCode() + result = 31 * result + scale.hashCode() + return result + } + + override fun toString(): String { + return "Position: ${position}\nRotation: ${rotation}\nScale: $scale\n" + } +} \ No newline at end of file diff --git a/toruslib/torus-math/src/main/java/com/google/android/torus/math/MathUtils.kt b/toruslib/torus-math/src/main/java/com/google/android/torus/math/MathUtils.kt new file mode 100644 index 0000000..85f4fda --- /dev/null +++ b/toruslib/torus-math/src/main/java/com/google/android/torus/math/MathUtils.kt @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2023 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.torus.math + +import kotlin.math.max +import kotlin.math.min + +/** + * Numeric operations and constants not included in the main Math class. + */ +object MathUtils { + const val DEG_TO_RAD = Math.PI / 180.0 + const val RAD_TO_DEG = 1 / DEG_TO_RAD + const val TAU = Math.PI * 2.0 + + /** + * Maps a value from a range to a different one (with the option to clamp it to the new range). + * + * @param value The value to map from a value range to a different one. + * @param inMin The minimum value of the original range. + * @param inMax The maximum value of the original range. + * @param outMin The minimum value of the new range. + * @param outMax The maximum value of the new range. + * @param clamp If you want to clamp the mapped value to the new range, set to true; otherwise + * set to false (by default is set to true). + * + * @return The [value] mapped to the new range. + */ + @JvmStatic + @JvmOverloads + fun map( + value: Double, + inMin: Double, + inMax: Double, + outMin: Double, + outMax: Double, + clamp: Boolean = true + ): Double { + if (clamp) { + if (value < inMin) { + return outMin + } + if (value > inMax) { + return outMax + } + } + return (value - inMin) / (inMax - inMin) * (outMax - outMin) + outMin + } + + /** + * Maps a value from a range to a different one (with the option to clamp it to the new range). + * + * @param value The value to map from a value range to a different one. + * @param inMin The minimum value of the original range. + * @param inMax The maximum value of the original range. + * @param outMin The minimum value of the new range. + * @param outMax The maximum value of the new range. + * @param clamp If you want to clamp the mapped value to the new range, set to true; otherwise + * set to false (by default is set to true). + * + * @return The [value] mapped to the new range. + */ + @JvmStatic + @JvmOverloads + fun map( + value: Float, + inMin: Float, + inMax: Float, + outMin: Float, + outMax: Float, + clamp: Boolean = true + ): Float { + if (clamp) { + if (value < inMin) { + return outMin + } + if (value > inMax) { + return outMax + } + } + return (value - inMin) / (inMax - inMin) * (outMax - outMin) + outMin + } + + /** + * Linear interpolation between two values. + * + * @param start The first value. + * @param end The second value. + * @param amount Decides the how we mix the interpolated values; when is 0, it returns the init + * value. When is 1, it returns the end value. For any value in between it returns the linearly + * interpolated value between [init] and [end]. If [amount] is smaller than 0 or bigger than 1 + * and [clamp] is false, it continues returning values based on the line created using + * [init] and [end]; otherwise the value is clamped to [init] and [end]. + * @param clamp If you want to clamp the mapped value to the new range, set to true; otherwise + * set to false (by default is set to true). + * + * @return The interpolated value. + */ + @JvmStatic + @JvmOverloads + fun lerp(start: Double, end: Double, amount: Double, clamp: Boolean = true): Double { + val amountClamped = if (clamp) { + clamp(amount, 0.0, 1.0) + } else { + amount + } + return (end - start) * amountClamped + start + } + + /** + * Linear interpolation between two values. + * + * @param init The first value. + * @param end The second value. + * @param amount Decides the how we mix the interpolated values; when is 0, it returns the init + * value. When is 1, it returns the end value. For any value in between it returns the linearly + * interpolated value between [init] and [end]. If [amount] is smaller than 0 or bigger than 1 + * and [clamp] is false, it continues returning values based on the line created using + * [init] and [end]; otherwise the value is clamped to [init] and [end]. + * @param clamp If you want to clamp the mapped value to the new range, set to true; otherwise + * set to false (by default is set to true). + * + * @return The interpolated value. + */ + @JvmStatic + @JvmOverloads + fun lerp(init: Float, end: Float, amount: Float, clamp: Boolean = true): Float { + val amountClamped = if (clamp) { + clamp(amount, 0.0f, 1.0f) + } else { + amount + } + return (end - init) * amountClamped + init + } + + /** + * Secures that a value is not smaller or bigger than the given range. + * + * @param value The input value. + * @param min The min value of the range. If [value] is smaller than this value, it is fixed to + * [min] value. + * @param max The max value of the range. If [value] is bigger than this value, it is fixed to + * [min] value. + * + * @return the value that is secured in the [[min], [max]] range. + */ + @JvmStatic + fun clamp(value: Double, min: Double, max: Double): Double { + return max(min(value, max), min) + } + + /** + * Secures that a value is not smaller or bigger than the given range. + * + * @param value The input value. + * @param min The min value of the range. If [value] is smaller than this value, it is fixed to + * [min] value. + * @param max The max value of the range. If [value] is bigger than this value, it is fixed to + * [min] value. + * + * @return the value that is secured in the [[min], [max]] range. + */ + @JvmStatic + fun clamp(value: Float, min: Float, max: Float): Float { + return max(min(value, max), min) + } +} diff --git a/toruslib/torus-math/src/main/java/com/google/android/torus/math/MatrixTransform.kt b/toruslib/torus-math/src/main/java/com/google/android/torus/math/MatrixTransform.kt new file mode 100644 index 0000000..e44da51 --- /dev/null +++ b/toruslib/torus-math/src/main/java/com/google/android/torus/math/MatrixTransform.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 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.torus.math + +/** + * Interface to make sure that the Transform classes that implement it return a transform matrix. + */ +interface MatrixTransform { + /** + * Returns a 4x4 transform Matrix. The format of the matrix follows the OpenGL ES matrix format + * stored in float arrays. + * Matrices are 4 x 4 column-vector matrices stored in column-major order: + * + *
+     *  m[0] m[4] m[8]  m[12]
+     *  m[1] m[5] m[9]  m[13]
+     *  m[2] m[6] m[10] m[14]
+     *  m[3] m[7] m[11] m[15]
+     *  
+ * + * @return a 16 value [FloatArray] representing the transform as a 4x4 matrix. + */ + fun toMatrix(): FloatArray +} \ No newline at end of file diff --git a/toruslib/torus-math/src/main/java/com/google/android/torus/math/RotationQuaternion.kt b/toruslib/torus-math/src/main/java/com/google/android/torus/math/RotationQuaternion.kt new file mode 100644 index 0000000..3a25da9 --- /dev/null +++ b/toruslib/torus-math/src/main/java/com/google/android/torus/math/RotationQuaternion.kt @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2023 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.torus.math + +import com.google.android.torus.math.MathUtils.DEG_TO_RAD +import com.google.android.torus.math.MathUtils.RAD_TO_DEG +import java.util.* +import kotlin.math.* + +/** + * A unit quaternion representing a rotation. + */ +class RotationQuaternion { + companion object { + /** + * Creates a rotation quaternion from a quaternion. + * + * @param w The w value of a quaternion. + * @param x The x value of a quaternion. + * @param y The y value of a quaternion. + * @param z The z value of a quaternion. + */ + @JvmStatic + fun fromQuaternion(w: Double, x: Double, y: Double, z: Double): RotationQuaternion { + val rotation = 2.0 * atan2(sqrt(x * x + y * y + z * z), w) * RAD_TO_DEG + val direction = Vector3(x.toFloat(), y.toFloat(), z.toFloat()).toNormalized() + return RotationQuaternion(rotation, direction) + } + + /** + * Creates a rotation quaternion from some Euler angles (ZYX sequence). + * + * @param eulerAngles The Euler rotation angles around each axis, in degrees. + */ + @JvmStatic + fun fromEuler(eulerAngles: Vector3): RotationQuaternion { + return fromEuler(eulerAngles.x, eulerAngles.y, eulerAngles.z) + } + + /** + * Creates a rotation quaternion from some Euler angles (ZYX sequence). + * + * @param rotationX The Euler rotation angle around the X axis, in degrees. + * @param rotationY The Euler rotation angle around the Y axis, in degrees. + * @param rotationZ The Euler rotation angle around the Z axis, in degrees. + */ + @JvmStatic + fun fromEuler(rotationX: Float, rotationY: Float, rotationZ: Float): RotationQuaternion { + val halfDegToRad = 0.5 * DEG_TO_RAD + val cy = cos(rotationZ * halfDegToRad) + val sy = sin(rotationZ * halfDegToRad) + val cp = cos(rotationY * halfDegToRad) + val sp = sin(rotationY * halfDegToRad) + val cr = cos(rotationX * halfDegToRad) + val sr = sin(rotationX * halfDegToRad) + + val w = cr * cp * cy + sr * sp * sy + val x = sr * cp * cy - cr * sp * sy + val y = cr * sp * cy + sr * cp * sy + val z = cr * cp * sy - sr * sp * cy + + return fromQuaternion(w, x, y, z) + } + } + + private val w: Double + private val x: Double + private val y: Double + private val z: Double + val direction: Vector3 + val angle: Double + + /** + * Creates a unit quaternion representing a rotation. + * + * @param angle The angle of rotation around [direction] vector, in degrees. The rotation is + * counterclockwise (if the [direction] vector is pointing at the point of sight). + * @param direction The angle of rotation, in degrees. + */ + constructor(angle: Double, direction: Vector3) { + this.direction = direction.toNormalized() + this.angle = angle + + val angleRad = angle * DEG_TO_RAD + val sinAngle = sin(angleRad) + w = cos(angleRad) + x = sinAngle * this.direction.x + y = sinAngle * this.direction.y + z = sinAngle * this.direction.z + } + + /** + * Creates a identity rotation quaternion, with the direction pointing to the X axis. + */ + constructor() : this(0.0, Vector3.X_AXIS) + + /** + * Creates a rotation quaternion from another rotation quaternion. + */ + constructor(rotationQuaternion: RotationQuaternion) : this( + rotationQuaternion.angle, + rotationQuaternion.direction + ) + + /** + * Returns a [Vector3] representing a quaternion as some Rotation Euler Angles (ZYX sequence). + * + * @return A [Vector3] containing the Euler rotation angle around each axis, in degrees. + */ + fun toEulerAngles(): Vector3 { + val angleX = atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y)) + + val tmp = 2 * (w * y - z * x) + val angleY = if (abs(tmp) >= 1) { + tmp.sign * PI / 2.0 + } else { + asin(tmp) + } + + val angleZ = atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)) + + return Vector3( + (angleX * RAD_TO_DEG).toFloat(), + (angleY * RAD_TO_DEG).toFloat(), + (angleZ * RAD_TO_DEG).toFloat() + ) + } + + /** + * Inverts the rotation quaternion (q^-1). + * + * @return The new inverted quaternion. + */ + fun inverse(): RotationQuaternion { + return fromQuaternion(w, -x, -y, -z) + } + + /** + * Applies the current rotation quaternion to a [Vector3]. + * + * @param vector the [Vector3] that will be rotated. + * + * @return the rotated [vector]. + */ + fun applyRotationTo(vector: Vector3): Vector3 { + return (this * (fromQuaternion( + 0.0, + vector.x.toDouble(), + vector.y.toDouble(), + vector.z.toDouble() + ) * this.inverse())).direction * vector.length() + } + + operator fun times(q: RotationQuaternion): RotationQuaternion { + return fromQuaternion( + w * q.w - x * q.x - y * q.y - z * q.z, + w * q.x + x * q.w + y * q.z - z * q.y, + w * q.y - x * q.z + y * q.w + z * q.x, + w * q.z + x * q.y - y * q.x + z * q.w + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RotationQuaternion + + if (direction != other.direction) return false + if (angle != other.angle) return false + + return true + } + + override fun hashCode(): Int { + var result = direction.hashCode() + result = 31 * result + angle.hashCode() + return result + } + + override fun toString(): String { + return "Angle: ${angle}º, Direction: $direction" + } +} \ No newline at end of file diff --git a/toruslib/torus-math/src/main/java/com/google/android/torus/math/SphericalTransform.kt b/toruslib/torus-math/src/main/java/com/google/android/torus/math/SphericalTransform.kt new file mode 100644 index 0000000..3da6bb1 --- /dev/null +++ b/toruslib/torus-math/src/main/java/com/google/android/torus/math/SphericalTransform.kt @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2023 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.torus.math + +import android.opengl.Matrix +import java.util.* +import kotlin.math.cos +import kotlin.math.sin + +/** + * Defines an immutable transformation using Spherical Coordinates, which might be more + * suitable for certain animations or behaviors than [AffineTransform] (i.e. camera orbit control). + * + * The position of a point P in Spherical Coordinates system is specified by: + * - A point [center], which defines the origin of spherical coordinate system. + * - A [distance] of the point P from the [center]. + * - And some polar angles [elevation] which defines the angle from the reference plane (which is + * parallel to the ZY plane and contains [center]) to the zenith direction (which is parallel to + * the Y axis and the normal of the reference plane, and passes though [center]) of the Point P; + * and [azimuth] that defines the angle of rotation of the projected point P into the + * reference plane (from Z to Y). + * + * In addition we add [roll] to the model (that rotates around the model's (intrinsic) Z axis), and + * [scale]. + * + * The order of operations is: scale => roll => Spherical Coordinates position and rotation. + */ +class SphericalTransform @JvmOverloads constructor( + /** The azimuth angle (from Z to X, counter clockwise, in degrees). */ + val azimuth: Float = 0f, + + /** + * The elevation angle (from ZX plane to Y axis; positive is up negative is down, in degrees). + */ + val elevation: Float = 0f, + + /** The roll rotation (around the model's (intrinsic) Z axis, in degrees). */ + val roll: Float = 0f, + + /** Center position of the spherical transform (the target). */ + val center: Vector3 = Vector3(0f, 0f, 0f), + + /** + * Distance of the transform from [center] which defines a sphere of radius = distance. + * The distance value has to be >= 0. + */ + val distance: Float = 1f, + + /** The scale of the transform. */ + val scale: Vector3 = Vector3(1f, 1f, 1f) + +) : MatrixTransform { + constructor(transform: SphericalTransform) : this( + transform.azimuth, + transform.elevation, + transform.roll, + transform.center, + transform.distance, + transform.scale + ) + + init { + if (distance < 0) throw IllegalArgumentException("Distance cannot be negative!") + } + + /** + * Creates a new [SphericalTransform] with a new [azimuth] rotation. + * + * @param azimuth The new azimuth rotation (from Z to X, counter clockwise, in degrees). + * + * @return The new [SphericalTransform] with the new rotation. + */ + fun withAzimuth(azimuth: Float): SphericalTransform { + return SphericalTransform(azimuth, elevation, roll, center, distance, scale) + } + + /** + * Creates a new [SphericalTransform] with a new [elevation] rotation. + * + * @param elevation The new elevation rotation (from ZX plane to Y axis; positive is up negative + * is down). In degrees. + * + * @return The new [SphericalTransform] with the new rotation. + */ + fun withElevation(elevation: Float): SphericalTransform { + return SphericalTransform(azimuth, elevation, roll, center, distance, scale) + } + + /** + * Creates a new [SphericalTransform] with a new [roll] rotation. + * + * @param roll The new elevation rotation (around Z axis, in degrees). + * + * @return The new [SphericalTransform] with the new rotation. + */ + fun withRoll(roll: Float): SphericalTransform { + return SphericalTransform(azimuth, elevation, roll, center, distance, scale) + } + + /** + * Creates a new [SphericalTransform] with a new [center]. + * + * @param center The center of the spherical transform. + * + * @return The new [SphericalTransform] with the new center. + */ + fun withCenter(center: Vector3): SphericalTransform { + return SphericalTransform(azimuth, elevation, roll, center, distance, scale) + } + + /** + * Creates a new [SphericalTransform] with a new ([x], [y], [z]) center. + * + * @param x The scale in the X direction. + * @param y The scale in the Y direction. + * @param z The scale in the Z direction. + * + * @return The new [SphericalTransform] with the new center. + */ + fun withCenter(x: Float, y: Float, z: Float): SphericalTransform { + return SphericalTransform(azimuth, elevation, roll, Vector3(x, y, z), distance, scale) + } + + /** + * Creates a new [SphericalTransform] with a new [distance] from the [center]. + * + * @param distance The new distance (cannot be smaller than 0). + * + * @return The new [SphericalTransform] with the new rotation. + */ + fun withDistance(distance: Float): SphericalTransform { + return SphericalTransform(azimuth, elevation, roll, center, distance, scale) + } + + /** + * Creates a new [SphericalTransform] with ([x], [y], [z]) as the new scale. + * + * @param x The scale in the X direction. + * @param y The scale in the Y direction. + * @param z The scale in the Z direction. + * + * @return The new [SphericalTransform] with the new scale. + */ + fun withScale(x: Float, y: Float, z: Float): SphericalTransform { + return SphericalTransform(azimuth, elevation, roll, center, distance, Vector3(x, y, z)) + } + + /** + * Creates a new [SphericalTransform] with ([scale], [scale], [scale]) as the new scale. + * + * @param scale The scale applied in the X,Y and Z directions. + * + * @return The new [SphericalTransform] with the new scale. + */ + fun withScale(scale: Float): SphericalTransform { + return withScale(scale, scale, scale) + } + + fun rotateByAzimuth(azimuth: Float): SphericalTransform { + return SphericalTransform( + this.azimuth + azimuth, + elevation, + roll, + center, + distance, + scale + ) + } + + fun rotateByElevation(elevation: Float): SphericalTransform { + return SphericalTransform( + azimuth, + this.elevation + elevation, + roll, + center, + distance, + scale + ) + } + + fun rollBy(roll: Float): SphericalTransform { + return SphericalTransform( + azimuth, + elevation, + this.roll + roll, + center, + distance, + scale + ) + } + + fun translateCenterBy(x: Float, y: Float, z: Float): SphericalTransform { + return SphericalTransform( + azimuth, + elevation, + roll, + center + Vector3(x, y, z), + distance, + scale + ) + } + + fun translateBy(distance: Float): SphericalTransform { + return SphericalTransform(azimuth, elevation, roll, center, this.distance + distance, scale) + } + + fun scaleBy(scale: Float): SphericalTransform { + return SphericalTransform( + azimuth, + elevation, + roll, + center, + distance, + this.scale + Vector3(scale) + ) + } + + fun scaleBy(x: Float, y: Float, z: Float): SphericalTransform { + return SphericalTransform( + azimuth, + elevation, + roll, + center, + distance, + scale + Vector3(x, y, z) + ) + } + + /** + * Returns a 4x4 transform Matrix. The format of the matrix follows the OpenGL ES matrix format + * stored in float arrays. + * Matrices are 4 x 4 column-vector matrices stored in column-major order: + * + *
+     *  m[0] m[4] m[8]  m[12]
+     *  m[1] m[5] m[9]  m[13]
+     *  m[2] m[6] m[10] m[14]
+     *  m[3] m[7] m[11] m[15]
+     *  
+ * + * @return a 16 value [FloatArray] representing the transform as a 4x4 matrix. + */ + override fun toMatrix(): FloatArray { + val transformMatrix = FloatArray(16) + val tmp = FloatArray(16) + val azimuthRad = (azimuth * MathUtils.DEG_TO_RAD).toFloat() + val elevationRad = (elevation * MathUtils.DEG_TO_RAD).toFloat() + Matrix.setIdentityM(transformMatrix, 0) + Matrix.scaleM(transformMatrix, 0, scale.x, scale.y, scale.z) + Matrix.rotateM(transformMatrix, 0, roll, 0f, 0f, 1f) + Matrix.setLookAtM( + tmp, 0, + center.x + distance * sin(azimuthRad) * cos(elevationRad), + center.y + distance * sin(elevationRad), + center.z + distance * cos(azimuthRad) * cos(elevationRad), + center.x, + center.y, + center.z, + Vector3.Y_AXIS.x, + Vector3.Y_AXIS.y, + Vector3.Y_AXIS.z + ) + Matrix.multiplyMM(transformMatrix, 0, tmp, 0, transformMatrix, 0) + return transformMatrix + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SphericalTransform + + if (azimuth != other.azimuth) return false + if (elevation != other.elevation) return false + if (roll != other.roll) return false + if (center != other.center) return false + if (distance != other.distance) return false + if (scale != other.scale) return false + + return true + } + + override fun hashCode(): Int { + var result = azimuth.hashCode() + result = 31 * result + elevation.hashCode() + result = 31 * result + roll.hashCode() + result = 31 * result + center.hashCode() + result = 31 * result + distance.hashCode() + result = 31 * result + scale.hashCode() + return result + } + + override fun toString(): String { + return "Rotation (az, el, ro): (${azimuth}, ${elevation}, ${roll})\n" + + "Center: $center\n" + + "Distance: $distance\n" + + "Scale: $scale\n" + } +} \ No newline at end of file diff --git a/toruslib/torus-math/src/main/java/com/google/android/torus/math/Vector2.kt b/toruslib/torus-math/src/main/java/com/google/android/torus/math/Vector2.kt new file mode 100644 index 0000000..aba0810 --- /dev/null +++ b/toruslib/torus-math/src/main/java/com/google/android/torus/math/Vector2.kt @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2023 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.torus.math + +import java.text.DecimalFormat +import java.util.* +import kotlin.math.hypot + +/** + * An immutable two-dimensional vector. + */ +class Vector2 @JvmOverloads constructor(val x: Float, val y: Float) { + companion object { + val ZERO = Vector2(0f, 0f) + val Y_AXIS = Vector2(0f, 1f) + val NEG_Y_AXIS = Vector2(0f, -1f) + val X_AXIS = Vector2(1f, 0f) + val NEG_X_AXIS = Vector2(-1f, 0f) + private val FORMAT = DecimalFormat("##.###") + + /** + * Linear interpolation between two vectors. The interpolated value will be always inside + * the interval [[vectorStart], [vectorEnd]]. + * + * @param vectorStart The first point that defines the linear interpolant. + * @param vectorEnd The second point that defines the linear interpolant. + * @param amount Value used to interpolate between [vectorStart] and [vectorEnd]. When + * [amount] is zero, the return value is [vectorStart]; when [amount] is 1, the return value + * is [vectorEnd]. + * + * @return interpolated value. + */ + @JvmStatic + fun lerp(vectorStart: Vector2, vectorEnd: Vector2, amount: Float): Vector2 { + return Vector2( + MathUtils.lerp(vectorStart.x, vectorEnd.x, amount), + MathUtils.lerp(vectorStart.y, vectorEnd.y, amount) + ) + } + } + + constructor() : this(0f) + constructor(value: Float) : this(value, value) + constructor(vector: Vector2) : this(vector.x, vector.y) + + fun plus(x: Float, y: Float): Vector2 { + return Vector2(this.x + x, this.y + y) + } + + fun minus(x: Float, y: Float): Vector2 { + return Vector2(this.x - x, this.y - y) + } + + fun multiplyBy(value: Float): Vector2 { + return multiplyBy(value, value) + } + + fun multiplyBy(vector: Vector2): Vector2 { + return multiplyBy(vector.x, vector.y) + } + + fun multiplyBy(x: Float, y: Float): Vector2 { + return Vector2(this.x * x, this.y * y) + } + + /** + * Returns a new [Vector2] instance with a normalize length (length = 1), and same direction + * as the current vector. + * + * @return the new [Vector2] instance with a normalized length and same direction as current + * vector. + */ + fun toNormalized(): Vector2 { + val length = length() + return Vector2(x / length, y / length) + } + + /** + * Performs the algebraic dot product operation. + * + * @param vector The second vector of the dot product. + * + * @return the result of the dot product of current vector and [vector]. + */ + fun dot(vector: Vector2): Float { + return dot(vector.x, vector.y) + } + + /** + * Performs the algebraic dot product operation. + * + * @param x The first component of a two-dimensional vector. + * @param y The second component of a two-dimensional vector. + * + * @return the result of the dot product of current vector and ([x], [y]). + */ + fun dot(x: Float, y: Float): Float { + return this.x * x + this.y * y + } + + /** + * Returns the distance between the current 2D point defined by current vector and [vector]. + * + * @param vector The second point. + * + * @return the distance between current vector and [vector]. + */ + fun distanceTo(vector: Vector2): Float { + return distanceTo(vector.x, vector.y) + } + + /** + * Returns the distance between the current 2D point defined by current vector + * and ([x], [y]). + * + * @param x The first component of a two-dimensional vector. + * @param y The second component of a two-dimensional vector. + * + * @return the distance between the current vector and ([x], [y]). + */ + fun distanceTo(x: Float, y: Float): Float { + return hypot(x - this.x, y - this.y) + } + + /** + * Returns the length of the current vector (which is the distance from origin (0, 0) to the + * position defined by the current vector). + * + * @return The length of the current vector. + */ + fun length(): Float { + return hypot(x, y) + } + + /** + * Returns a new vector with same direction as the current vector and a [newLength] length. + * + * @param newLength the new length of the vector + * + * @return the new [Vector2] instance with a [newLength] and same direction as current vector. + */ + fun withLength(newLength: Float): Vector2 { + return times(newLength / length()) + } + + operator fun minus(vector: Vector2): Vector2 { + return minus(vector.x, vector.y) + } + + operator fun plus(vector: Vector2): Vector2 { + return plus(vector.x, vector.y) + } + + operator fun times(value: Float): Vector2 { + return multiplyBy(value) + } + + operator fun times(vector: Vector2): Vector2 { + return multiplyBy(vector) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Vector2 + + if (x != other.x) return false + if (y != other.y) return false + + return true + } + + override fun hashCode(): Int { + var result = x.hashCode() + result = 31 * result + y.hashCode() + return result + } + + override fun toString(): String { + return "(${FORMAT.format(x)}, ${FORMAT.format(y)})" + } +} \ No newline at end of file diff --git a/toruslib/torus-math/src/main/java/com/google/android/torus/math/Vector3.kt b/toruslib/torus-math/src/main/java/com/google/android/torus/math/Vector3.kt new file mode 100644 index 0000000..e55c139 --- /dev/null +++ b/toruslib/torus-math/src/main/java/com/google/android/torus/math/Vector3.kt @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2023 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.torus.math + +import java.text.DecimalFormat +import java.util.* +import kotlin.math.sqrt + +/** + * An immutable three-dimensional vector. + */ +class Vector3 @JvmOverloads constructor(val x: Float, val y: Float, val z: Float) { + companion object { + val ZERO = Vector3(0f, 0f, 0f) + val Y_AXIS = Vector3(0f, 1f, 0f) + val NEG_Y_AXIS = Vector3(0f, -1f, 0f) + val X_AXIS = Vector3(1f, 0f, 0f) + val NEG_X_AXIS = Vector3(-1f, 0f, 0f) + val Z_AXIS = Vector3(0f, 0f, 1f) + val NEG_Z_AXIS = Vector3(0f, 0f, -1f) + private val FORMAT = DecimalFormat("##.###") + + /** + * Linear interpolation between two vectors. The interpolated value will be always inside + * the interval [[vectorStart], [vectorEnd]]. + * + * @param vectorStart The first point that defines the linear interpolant. + * @param vectorEnd The second point that defines the linear interpolant. + * @param amount Value used to interpolate between [vectorStart] and [vectorEnd]. When + * [amount] is zero, the return value is [vectorStart]; when [amount] is 1, the return value + * is [vectorEnd]. + * + * @return interpolated value. + */ + @JvmStatic + fun lerp(vectorStart: Vector3, vectorEnd: Vector3, amount: Float): Vector3 { + return Vector3( + MathUtils.lerp(vectorStart.x, vectorEnd.x, amount), + MathUtils.lerp(vectorStart.y, vectorEnd.y, amount), + MathUtils.lerp(vectorStart.z, vectorEnd.z, amount) + ) + } + + /** + * Calculates the cross product from [firstVector] to [secondVector] + * (that means [firstVector]x[secondVector]). + * + * @param firstVector The first three-dimensional vector. + * @param secondVector The second three-dimensional vector. + * + * @return A [Vector3] that represents the result of [firstVector]x[secondVector]. + */ + @JvmStatic + fun cross(firstVector: Vector3, secondVector: Vector3): Vector3 { + return Vector3( + firstVector.y * secondVector.z - firstVector.z * secondVector.y, + firstVector.z * secondVector.x - firstVector.x * secondVector.z, + firstVector.x * secondVector.y - firstVector.y * secondVector.x + ) + } + } + + constructor() : this(0f) + constructor(value: Float) : this(value, value, value) + constructor(vector: Vector3) : this(vector.x, vector.y, vector.z) + + + fun plus(x: Float, y: Float, z: Float): Vector3 { + return Vector3(this.x + x, this.y + y, this.z + z) + } + + fun minus(x: Float, y: Float, z: Float): Vector3 { + return Vector3(this.x - x, this.y - y, this.z - z) + } + + fun multiplyBy(value: Float): Vector3 { + return multiplyBy(value, value, value) + } + + fun multiplyBy(vector: Vector3): Vector3 { + return multiplyBy(vector.x, vector.y, vector.z) + } + + fun multiplyBy(x: Float, y: Float, z: Float): Vector3 { + return Vector3(this.x * x, this.y * y, this.z * z) + } + + /** + * Returns a new [Vector3] instance with a normalize length (length = 1), and same direction + * as the current vector. + * + * @return the new [Vector3] instance with a normalized length and same direction as current + * vector. + */ + fun toNormalized(): Vector3 { + val length = length() + return Vector3(x / length, y / length, z / length) + } + + /** + * Performs the algebraic dot product operation. + * + * @param vector The second vector of the dot product. + * + * @return the result of the dot product of current vector and [vector]. + */ + fun dot(vector: Vector3): Float { + return dot(vector.x, vector.y, vector.z) + } + + /** + * Performs the algebraic dot product operation. + * + * @param x The first component of a three-dimensional vector. + * @param y The second component of a three-dimensional vector. + * @param y The third component of a three-dimensional vector. + * + * @return the result of the dot product of current vector and ([x], [y], [z]). + */ + fun dot(x: Float, y: Float, z: Float): Float { + return this.x * x + this.y * y + this.z * z + } + + /** + * Returns the distance between the current 3D point defined by current vector and [vector]. + * + * @param vector The second point. + * + * @return the distance between current vector and [vector]. + */ + fun distanceTo(vector: Vector3): Float { + return distanceTo(vector.x, vector.y, vector.z) + } + + /** + * Returns the distance between the current 3D point defined by current vector + * and ([x], [y], [z]). + * + * @param x The first component of a three-dimensional vector. + * @param y The second component of a three-dimensional vector. + * @param z The third component of a three-dimensional vector. + * + * @return the distance between current vector and ([x], [y], [z]). + */ + fun distanceTo(x: Float, y: Float, z: Float): Float { + val dx = x - this.x + val dy = y - this.y + val dz = z - this.z + return sqrt(dx * dx + dy * dy + dz * dz) + } + + /** + * Returns the length of the current vector (which is the distance from origin (0, 0, 0) to the + * position defined by the current vector). + * + * @return The length of the current vector. + */ + fun length(): Float { + return sqrt(dot(x, y, z)) + } + + /** + * Returns a new vector with same direction as the current vector and a [newLength] length. + * + * @param newLength the new length of the vector + * + * @return the new [Vector3] instance with a [newLength] and same direction as current vector. + */ + fun withLength(newLength: Float): Vector3 { + return multiplyBy(newLength / length()) + } + + operator fun minus(vector: Vector3): Vector3 { + return minus(vector.x, vector.y, vector.z) + } + + operator fun plus(vector: Vector3): Vector3 { + return plus(vector.x, vector.y, vector.z) + } + + operator fun times(value: Float): Vector3 { + return multiplyBy(value) + } + + operator fun times(vector: Vector3): Vector3 { + return multiplyBy(vector) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Vector3 + + if (x != other.x) return false + if (y != other.y) return false + if (z != other.z) return false + + return true + } + + override fun hashCode(): Int { + var result = x.hashCode() + result = 31 * result + y.hashCode() + result = 31 * result + z.hashCode() + return result + } + + override fun toString(): String { + return "(${FORMAT.format(x)}, ${FORMAT.format(y)}, ${FORMAT.format(z)})" + } +} \ No newline at end of file diff --git a/toruslib/torus-utils/build.gradle b/toruslib/torus-utils/build.gradle new file mode 100644 index 0000000..689e4fb --- /dev/null +++ b/toruslib/torus-utils/build.gradle @@ -0,0 +1,17 @@ +// Copyright (C) 2023 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. + +dependencies { + implementation project(':torus-math') +} \ No newline at end of file diff --git a/toruslib/torus-utils/src/main/AndroidManifest.xml b/toruslib/torus-utils/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d7de763 --- /dev/null +++ b/toruslib/torus-utils/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/BitmapUtils.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/BitmapUtils.kt new file mode 100644 index 0000000..dadb6b5 --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/BitmapUtils.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 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.torus.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.util.Size +import android.view.View +import java.io.IOException + +/** + * Bitmap utils. + */ +object BitmapUtils { + @JvmStatic + @Throws(IOException::class) + fun loadBitmap(context: Context, bitmapResourceId: Int): Bitmap { + val options = BitmapFactory.Options() + options.inScaled = false + return BitmapFactory.decodeResource(context.resources, bitmapResourceId, options) + ?: throw IOException("Bitmap has not been decoded properly.") + } + + @JvmStatic + fun getBitmapSize(context: Context, bitmapResourceId: Int): Size { + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + val bitmap = BitmapFactory.decodeResource(context.resources, bitmapResourceId, options) + val size = Size(options.outWidth, options.outHeight) + bitmap?.recycle() + return size + } + + /** + * Generates a Bitmap from a view. + * + * @param view The view that we want to create a [Bitmap]. + * @param config The [Bitmap.Config] of how we load the view. By default uses + * an ARGB 8888 format. + * + * @return The generated [Bitmap] + */ + @JvmStatic + fun generateBitmapFromView( + view: View, + config: Bitmap.Config = Bitmap.Config.ARGB_8888 + ): Bitmap { + val bitmap = Bitmap.createBitmap(view.measuredWidth, view.measuredHeight, config) + val canvas = Canvas(bitmap) + canvas.drawColor(Color.TRANSPARENT) + view.draw(canvas) + return bitmap + } +} \ No newline at end of file diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/animation/EasingUtils.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/animation/EasingUtils.kt new file mode 100644 index 0000000..d9b33a4 --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/animation/EasingUtils.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 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.torus.utils.animation + +import com.google.android.torus.math.MathUtils +import kotlin.math.pow + +/** Utilities to help implement "easing" operations. */ +object EasingUtils { + /** + * Easing function to interpolate a smooth curve that "follows" some other signal value in + * real-time. The "follow curve" is an exponentially-weighted moving average (EWMA) of the + * signal values: assuming a fixed timestep, the "follow value" at time t is determined by the + * signal value S_t as `F_t = k * S_t + (1 - k) * F_(t-1)`, for some "easing rate" k between + * 0 and 1. Note this formulation assumes that the "signal curve" moves by discrete steps with + * zero velocity in between. This may cause slightly unexpected "follow" behavior -- e.g. the + * curve may start to "settle" toward the new signal value even if we don't expect it to be + * stable, or it may lag and/or move somewhat abruptly if the "signal curve" reverses direction. + * These discrepancies would be most noticeable at frame rates that are especially low or + * highly-variable, and so far they haven't seemed problematic in any of our applications. + * + * @param currentValue The value of the "follow curve" prior to this update step (i.e., either + * the value returned the last time this function was called, or the initial value where the + * follow curve should start). In most applications the initial value will be set to match + * the first reading of the signal value. + * @param targetValue The most recent reading of the "signal value." If this value remains + * constant, the "follow curve" will eventually settle to it (asymptotically). + * @param easingRate A parameter to control the "follow speed" between 0 (the follow curve + * remains at its |currentValue| regardless of the new signal) and 1 (the follow curve + * immediately snaps to the new |targetValue|, effectively disabling easing). + * This parameter is typically tuned empirically. If the simulation is running at 60FPS, the + * easing function exactly matches the "fixed timestep" version above, with easing rate k. + * @param deltaSeconds The amount of time elapsed since determining the old |currentValue|, in + * seconds, during which the "follow curve" is assumed to have been converging towards the new + * |targetValue|. + * + * @return the value of the "easing curve" after updating by |deltaSeconds|. + */ + @JvmStatic + fun calculateEasing( + currentValue: Float, targetValue: Float, easingRate: Float, deltaSeconds: Float + ): Float { + /* The exponential form of easing we use to support variable frame rates is inverted from + * the fixed timestep version above; an easing rate of zero "disables easing" so that the + * follow curve "snaps" to the new value, while an easing rate of one leaves the follow + * curve at its current value. We can simply take the complement: */ + val exponentialEasingRate = 1f - easingRate + + val lerpBy = 1f - exponentialEasingRate.pow(deltaSeconds) + return MathUtils.lerp(currentValue, targetValue, lerpBy) + } +} diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/broadcast/BroadcastEventController.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/broadcast/BroadcastEventController.kt new file mode 100644 index 0000000..f25d498 --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/broadcast/BroadcastEventController.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2023 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.torus.utils.broadcast + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import java.util.concurrent.atomic.AtomicBoolean + +/** + * This is the base class to be implemented when we need to listen to broadcast events + * It registers a broadcast receiver and triggers [onBroadcastReceived] when a new + * broadcast is received. + */ +abstract class BroadcastEventController constructor(protected var context: Context) { + private val broadcastRegistered: AtomicBoolean = AtomicBoolean(false) + private val initialized: AtomicBoolean + private val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + intent.action?.let { action -> + onBroadcastReceived(context, intent, action) + } + } + } + + init { + val hasInitialized = initResources() + initialized = AtomicBoolean(hasInitialized) + } + + protected abstract fun initResources(): Boolean + abstract fun onBroadcastReceived(context: Context, intent: Intent, action: String) + protected abstract fun onRegister(fire: Boolean): IntentFilter + protected abstract fun onUnregister() + + /** + * Start listening to broadcasts by registering the broadcast receiver. + * + * @param fire sets whether to notify the listener right away with the current state. + */ + fun start(fire: Boolean = false) { + if (!initialized.get()) { + val hasInitialized = initResources() + if (!hasInitialized) return + initialized.set(true) + } + if (!broadcastRegistered.get()) { + val filter = onRegister(fire) + registerReceiver(context, broadcastReceiver, filter) + broadcastRegistered.set(true) + } + } + + /** + * Stop listening to broadcasts by unregistering the broadcast receiver. + */ + @Synchronized + fun stop() { + if (broadcastRegistered.get()) { + onUnregister() + unregisterReceiver(context, broadcastReceiver) + broadcastRegistered.set(false) + } + } + + protected fun registerReceiver( + context: Context, + broadcastReceiver: BroadcastReceiver?, + filter: IntentFilter? + ) { + context.registerReceiver(broadcastReceiver, filter) + } + + protected fun unregisterReceiver(context: Context, broadcastReceiver: BroadcastReceiver?) { + context.unregisterReceiver(broadcastReceiver) + } +} diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/broadcast/PowerSaveController.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/broadcast/PowerSaveController.kt new file mode 100644 index 0000000..c9f0d3e --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/broadcast/PowerSaveController.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 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.torus.utils.broadcast + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.PowerManager +import java.util.concurrent.atomic.AtomicBoolean + +/** + * PowerSaveController registers a BroadcastReceiver that listens to + * changes in Power Save Mode provided by the OS. + * Forwards received broadcasts to be handled by a [PowerSaveListener]. + */ +class PowerSaveController( + context: Context, + private val listener: PowerSaveListener? +) : BroadcastEventController(context) { + companion object { + const val DEFAULT_POWER_SAVE_MODE = false + } + + private var powerSaving: AtomicBoolean? = null + private var powerManager: PowerManager? = null + + override fun initResources(): Boolean { + if (powerSaving == null) powerSaving = AtomicBoolean(DEFAULT_POWER_SAVE_MODE) + if (powerManager == null) powerManager = + context.getSystemService(Context.POWER_SERVICE) as PowerManager? + return powerManager != null + } + + override fun onBroadcastReceived(context: Context, intent: Intent, action: String) { + if (action == PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) { + /* Check if powerSaveMode has changed. */ + powerManager?.let { setPowerSave(it.isPowerSaveMode, true) } + } + } + + override fun onRegister(fire: Boolean): IntentFilter { + powerManager?.let { setPowerSave(it.isPowerSaveMode, fire) } + return IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) + } + + override fun onUnregister() {} + + private fun setPowerSave(isPowerSave: Boolean, fire: Boolean) { + powerSaving?.let { + if (it.get() == isPowerSave) return + it.set(isPowerSave) + } + + listener?.let { + if (fire) listener.onPowerSaveModeChanged(isPowerSave) + } + } + + fun isPowerSaving(): Boolean = powerSaving?.get() ?: false + + interface PowerSaveListener { + fun onPowerSaveModeChanged(isPowerSaveMode: Boolean) + } + +} diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/content/ResourcesManager.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/content/ResourcesManager.kt new file mode 100644 index 0000000..0e94530 --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/content/ResourcesManager.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 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.torus.utils.content + +import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap + +/** + * Class that holds the resources to be used by the engine in concurrent instances. + * This class is used to re-use resources that take time and memory in the system. Adding them here + * they can be re-used by different instances. + */ +class ResourcesManager { + private val resources: ConcurrentMap> = ConcurrentHashMap(0) + + /** + * The number of resources hold. + */ + var size: Int = 0 + get() = resources.size + private set + + /** + * Stores the given resource in the ResourceManager. + * + * @param key A string identifying the resource (it can be any). + * @param resource The resource that we want to use in multiple instances. + * @return True if the resource was added; false if the resource already existed. + */ + fun addResource(key: String, resource: Any): Boolean { + if (resources.contains(key) && + resources[key] != null && + resources[key]!!.get() != null + ) { + return false + } + + resources[key] = WeakReference(resource) + return true + } + + /** + * Gets a resource from the ResourcesManager, using the supplied function to create the resource + * if it didn't already exist. The key is always mapped to a non-null resource as a + * post-condition of this method. + * + * @param key A string identifying the resource (it can be any). + * @param provider A function to create the resource if it's not already indexed. + * @return The (new or existing) resource associated with the key. + */ + fun getOrAddResource(key: String, provider: () -> T): T { + val resource: T = (resources[key]?.get() as T) ?: provider() + resources[key] = WeakReference(resource) + return resource + } + + /** + * Returns the resource associated with the [key]. + * + * @param key The key associated with the resource. + * @return The given resource; null if the resource wasn't found. + */ + fun getResource(key: String): Any? { + if (!resources.contains(key)) return null + resources[key]?.let { + return it.get() + } + return null + } + + /** + * Stops the resource associated with the given [key]. + * + * @param key The key associated with the resource. + * @return The resource reference that has been removed; null if the resource wasn't found. + */ + fun removeResource(key: String): WeakReference? = resources.remove(key) +} diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/display/DisplayOrientationController.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/display/DisplayOrientationController.kt new file mode 100644 index 0000000..7d2379e --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/display/DisplayOrientationController.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2023 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.torus.utils.display + +import android.content.Context +import android.hardware.display.DisplayManager +import android.os.Build +import android.view.Display +import android.view.Surface +import android.view.WindowManager + + +/** + * Listens to rotation changes of the current display (read from the context for Android 11+) + * or the orientation of default display (For version <= Android 10). + */ +class DisplayOrientationController( + context: Context, + private val listener: DisplayOrientationListener? = null +) { + /** + * The orientation of the screen. we have two types: + * [DisplayOrientation.NATURAL_ORIENTATION]: The default orientation of the device + * (for a phone it is portrait). + * + * [DisplayOrientation.ALTERNATE_ORIENTATION]: When we rotate the device ± 90º from the + * default orientation (for a phone it is landscape). + */ + enum class DisplayOrientation { NATURAL_ORIENTATION, ALTERNATE_ORIENTATION } + + var rotation = Surface.ROTATION_0 + private set + var orientation = DisplayOrientation.NATURAL_ORIENTATION + private set + private val displayChangeListener: DisplayManager.DisplayListener = + object : DisplayManager.DisplayListener { + override fun onDisplayAdded(displayId: Int) { + if (displayId == display.displayId) updateRotationAndOrientation() + } + + override fun onDisplayRemoved(displayId: Int) { + if (displayId == display.displayId) updateRotationAndOrientation() + } + + override fun onDisplayChanged(displayId: Int) { + if (displayId == display.displayId) updateRotationAndOrientation() + } + + } + private val displayManager: DisplayManager = + context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + private val display: Display + + init { + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + + // Only available for Android 11 (SDK 30); before that we can only know the default display. + display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + context.display ?: windowManager.defaultDisplay + } else { + windowManager.defaultDisplay + } + + updateRotationAndOrientation(false) + } + + /** Starts listening for display rotation/orientation changes. */ + fun start() { + updateRotationAndOrientation() + displayManager.registerDisplayListener(displayChangeListener, null) + } + + /** + * Requests an update on rotation and orientation. If there are changes, + * [DisplayOrientationListener.onDisplayOrientationChanged] will be called. + */ + fun update() = updateRotationAndOrientation() + + /** Stops listening for display rotation/orientation changes. */ + fun stop() = displayManager.unregisterDisplayListener(displayChangeListener) + + private fun updateRotationAndOrientation(sendEvent: Boolean = true) { + val rotationTmp = display.rotation + + if (rotation != rotationTmp) { + + rotation = rotationTmp + orientation = when (rotation) { + Surface.ROTATION_90 -> DisplayOrientation.ALTERNATE_ORIENTATION + Surface.ROTATION_270 -> DisplayOrientation.ALTERNATE_ORIENTATION + else -> DisplayOrientation.NATURAL_ORIENTATION + } + + if (sendEvent) listener?.onDisplayOrientationChanged(orientation, rotation) + } + } + + /** Interface to listen to display orientation changes. */ + interface DisplayOrientationListener { + /** + * Called when orientation has changed for the [Display] that [DisplayOrientationController] + * is currently tracking. + * + * @param orientation the new [DisplayOrientationController.DisplayOrientation] orientation. + * @param rotation the new rotation (it can be [Surface.ROTATION_0], [Surface.ROTATION_90], + * [Surface.ROTATION_180] or [Surface.ROTATION_270]). + */ + fun onDisplayOrientationChanged(orientation: DisplayOrientation, rotation: Int) + } +} diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/display/DisplaySizeType.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/display/DisplaySizeType.kt new file mode 100644 index 0000000..ceeda6f --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/display/DisplaySizeType.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 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.torus.utils.display + +import android.content.res.Configuration +import android.view.Display + +/** + * Class that defines the type of display based on its size. The types or displays align with + * the Window size classes and thresholds defined in: + * https://developer.android.com/guide/topics/large-screens/support-different-screen-sizes. + */ +enum class DisplaySizeType { + COMPACT, + MEDIUM, + EXPANDED; + + companion object { + private const val MEDIUM_WIDTH_DP_THRESHOLD: Float = 600f + private const val EXPANDED_WIDTH_DP_THRESHOLD: Float = 840f + private const val MEDIUM_HEIGHT_DP_THRESHOLD: Float = 480f + private const val EXPANDED_HEIGHT_DP_THRESHOLD: Float = 900f + + /** + * Returns the [DisplaySizeType] based on the display's width. + * + * @param width the current width of the display (in dp). + * + * @return The [DisplaySizeType] based on the display [width]. + */ + @JvmStatic + fun fromWidth(width: Float): DisplaySizeType { + return when { + width < MEDIUM_WIDTH_DP_THRESHOLD -> COMPACT + width < EXPANDED_WIDTH_DP_THRESHOLD -> MEDIUM + else -> EXPANDED + } + } + + /** + * Returns the [DisplaySizeType] based the display's height. + * + * @param height the current height of the display (in dp). + * + * @return The [DisplaySizeType] based on the display [height]. + */ + @JvmStatic + fun fromHeight(height: Float): DisplaySizeType { + return when { + height < MEDIUM_HEIGHT_DP_THRESHOLD -> COMPACT + height < EXPANDED_HEIGHT_DP_THRESHOLD -> MEDIUM + else -> EXPANDED + } + } + + /** + * Returns the smallest [DisplaySizeType] available (that means for any orientation + * of the device) for the current Screen associated to the [config]. This can help + * understand what kind of display we have (i.e., if the returned value is + * [DisplaySizeType.MEDIUM], we know that this display, independently of its + * orientation will have a screen width (in dp) >= MEDIUM_WIDTH_DP_THRESHOLD. + * + * @param config the current [Configuration]. + * + * @return The smallest [DisplaySizeType] that the current display will have (of all + * possible orientations). + */ + @JvmStatic + fun smallestAvailableFromDisplay(display: Display): DisplaySizeType { + /* + * if we were using a Configuration object and we only wanted the display/screen + * associated with the current configuration, we could use + * config.smallestScreenWidthDp.toFloat() instead. + */ + return fromWidth(DisplayUtils.getSmallestDisplayWidthDp(display)) + } + } +} + diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/display/DisplayUtils.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/display/DisplayUtils.kt new file mode 100644 index 0000000..248988e --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/display/DisplayUtils.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2023 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.torus.utils.display + +import android.content.Context +import android.graphics.Point +import android.hardware.display.DisplayManager +import android.util.DisplayMetrics +import android.util.Size +import android.view.Display +import kotlin.math.round + +/** Display-related utils. */ +object DisplayUtils { + + /** A constant value that should be passed in to get all the screens. */ + private const val DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED = + "android.hardware.display.category.ALL_INCLUDING_DISABLED" + /** + * Returns a list of the ID and size of each Display available on the current device. + * + * @param context the application context. + * + * @return a [List] composed by [Pair] of Display ID and Display Size. + */ + @JvmStatic + fun getDisplayIdsAndSizes(context: Context): List> { + val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + val displays = displayManager.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) + val displaySizes = displays.map { + val size = Point() + /* + * Note: this API has been deprecated but currently there isn't a good alternative. + * The proposed way in the Android Developers site: + * + * ``` + * This method was deprecated in API level 31. + * Use WindowManager#getCurrentWindowMetrics() to identify the current size of + * the activity window. UI-related work, such as choosing UI layouts, + * should rely upon WindowMetrics#getBounds(). + * ``` + * + * Only works to retrieve the DEFAULT/current display but not for any display. + * Once we have an API that allows to retrieve this information, this code will be + * updated. + */ + it.getRealSize(size) + Pair(it.displayId, Size(size.x, size.y)) + } + + return displaySizes + } + + /** + * Returns the number of available displays. + * + * @param context the application context. + * + * @return the number of available displays. + */ + @JvmStatic + fun getNumberOfDisplaysAvailable(context: Context): Int { + val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + return displayManager.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED).size + } + + /** + * Converts a pixel unit to dp. + * + * @param pixels the pixels unit to convert. + * @param displayMetrics the [DisplayMetrics] we want to use. + * + * @return the pixel unit converted to dp. + */ + @JvmStatic + fun convertPixelToDp(pixels: Float, displayMetrics: DisplayMetrics): Float { + val densityRatio = displayMetrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat() + return pixels / densityRatio + } + + /** + * Converts a dp unit to pixels. + * + * @param dp the dp unit to convert. + * @param displayMetrics the [DisplayMetrics] we want to use. + * + * @return the dp unit converted to pixels. + */ + @JvmStatic + fun convertDpToPixel(dp: Float, displayMetrics: DisplayMetrics): Float { + val densityRatio = displayMetrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat() + return round(dp * densityRatio) + } + + /** + * Returns the smallest [Display] width in dp (that means for any orientation of the device). + * + * @param display the [Display] we want to get the smallest width in dp. + * + * @return The smallest width in dp. + */ + fun getSmallestDisplayWidthDp(display: Display): Float { + val metrics = DisplayMetrics() + /* + * Note: this API has been deprecated but currently there isn't a good alternative. + * The proposed way in the Android Developers site: + * + * ``` + * This method was deprecated in API level 31. + * Use WindowManager#getCurrentWindowMetrics() to identify the current size of + * the activity window. UI-related work, such as choosing UI layouts, + * should rely upon WindowMetrics#getBounds(). + * ``` + * + * Only works to retrieve the DEFAULT/current display but not for any display. + * Once we have an API that allows to retrieve this information, this code will be + * updated. + */ + display.getRealMetrics(metrics) + return convertPixelToDp( + minOf(metrics.widthPixels, metrics.heightPixels).toFloat(), + metrics + ) + } +} diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/extensions/ActivityExt.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/extensions/ActivityExt.kt new file mode 100644 index 0000000..d0dade4 --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/extensions/ActivityExt.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 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.torus.utils.extensions + +import android.app.Activity +import android.view.View +import android.view.WindowInsets +import android.view.WindowInsetsController + +/** + * Extends [Activity] to allow to set immersive fullscreen mode. + */ +fun Activity.setImmersiveFullScreen() { + // Sets into Immersive mode. + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(false) + + // Hides Navigation bar and Status bar. + val controller = window.insetsController + + if (controller != null) { + controller.hide( + WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars() + ) + controller.systemBarsBehavior = + WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } else { + // Sets into Immersive mode on older APIs. + window.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_IMMERSIVE + // Set the content to appear under the system bars so that the + // content doesn't resize when the system bars hide and show. + or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + // Hide the nav bar and status bar + or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_FULLSCREEN + ) + } +} diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/extensions/AssetManagerExt.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/extensions/AssetManagerExt.kt new file mode 100644 index 0000000..7f788b0 --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/extensions/AssetManagerExt.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 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.torus.utils.extensions + +import android.content.res.AssetManager +import java.nio.ByteBuffer +import java.nio.channels.Channels + +/** + * Extends [AssetManager] to read uncompressed assets. + * + * @param assetPathAndName The string of the asset path and name inside the assets folder. + * The asset must be uncompressed. + * + * @return A [ByteBuffer] containing the asset. + */ +fun AssetManager.readUncompressedAsset(assetPathAndName: String): ByteBuffer { + + openFd(assetPathAndName).use { fd -> + val input = fd.createInputStream() + val dst = ByteBuffer.allocate(fd.length.toInt()) + + val src = Channels.newChannel(input) + src.read(dst) + src.close() + + return dst.apply { rewind() } + } +} + +/** + * Extends [AssetManager] to read assets. + * + * @param assetPathAndName The string of the asset path and name inside the assets folder. + * @return A [ByteBuffer] containing the asset. + */ +fun AssetManager.readAsset(assetPathAndName: String): ByteBuffer { + open(assetPathAndName).use { inputStream -> + val byteArray = inputStream.readBytes() + + val dst = ByteBuffer.allocate(byteArray.size) + dst.put(byteArray) + inputStream.close() + + return dst.apply { rewind() } + } +} + +/** + * Extends [AssetManager] to read an asset as a [String]. + * + * @param assetName The string of the asset inside the assets folder. + * @return A [String] containing the asset information. + */ +fun AssetManager.readAssetAsString(assetName: String): String { + return String(readAsset(assetName).array(), Charsets.ISO_8859_1) +} diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/extensions/SizeExt.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/extensions/SizeExt.kt new file mode 100644 index 0000000..e92f2c7 --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/extensions/SizeExt.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 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.torus.utils.extensions + +import android.util.Size +import android.util.SizeF + +/** + * Extends [Size] to return the aspect ratio (ratio between the width and height). This ratio is + * returned as the value resulting of the operation width / height. If width or height have invalid + * values (smaller or equal to 0), -1 is returned. + * + * @return the [Float] representing the aspect ratio, or -1 if width or height have invalid values + * (smaller or equal to 0). + */ +fun Size.getAspectRatio(): Float { + return if (height <= 0 || width <= 0) { + -1f + } else { + width / height.toFloat() + } +} + +/** + * Extends [Size] to return the aspect ratio (ratio between the width and height). This ratio is + * returned as the value resulting of the operation width / height. If width or height have invalid + * values (smaller or equal to 0), -1 is returned. + * + * @return the [Float] representing the aspect ratio, or -1 if width or height have invalid values + * (smaller or equal to 0). + */ +fun SizeF.getAspectRatio(): Float { + return if (height <= 0 || width <= 0) { + -1f + } else { + width / height + } +} \ No newline at end of file diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/interaction/Gyro2dController.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/interaction/Gyro2dController.kt new file mode 100644 index 0000000..20b3236 --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/interaction/Gyro2dController.kt @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2023 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.torus.utils.interaction + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.util.Log +import android.view.Surface +import com.google.android.torus.math.MathUtils +import com.google.android.torus.math.Vector2 +import kotlin.math.abs +import kotlin.math.sign + +/** + * Class that analyzed the gyroscope and generates a rotation out of it. + * This class only calculates the gyroscope rotation for two angles/degrees: + * - Pitch (rotation around device X axis). + * - Yaw (rotation around device Y axis). + * + * (Check https://developer.android.com/guide/topics/sensors/sensors_motion for more info). + */ +class Gyro2dController(context: Context, config: GyroConfig = GyroConfig()) { + companion object { + private const val TAG = "Gyro2dController" + const val NANOS_TO_S = 1.0f / 1_000_000_000.0f + const val RAD_TO_DEG = (180f / Math.PI).toFloat() + const val BASE_FPS = 60f + const val DEFAULT_EASING = 0.8f + } + + /** + * Defines the final rotation. + * + * - [Vector2.x] represents the Pitch (in degrees). + * - [Vector2.y] represents the Yaw (in degrees). + */ + var rotation: Vector2 = Vector2() + private set + + /** + * Defines if gyro is considered to be settled. + * TODO: remove once clients are switched to the new |isCurrentlySettled(Vector2)| API. + */ + var isSettled: Boolean = false + private set + + /** + * Defines whether the gyro animation is almost settled. + * TODO: remove once clients are switched to the new |isNearlySettled(Vector2)| API. + */ + var isAlmostSettled: Boolean = false + private set + + /** + * The config that defines the behavior of the gyro.. + */ + var config: GyroConfig = config + set(value) { + field = value + onNewConfig() + } + + private val angles: FloatArray = FloatArray(3) + private val sensorEventListener: SensorEventListener = object : SensorEventListener { + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + } + + override fun onSensorChanged(event: SensorEvent) { + updateGyroRotation(event) + } + } + private val displayRotationValues: IntArray = + intArrayOf( + Surface.ROTATION_0, + Surface.ROTATION_90, + Surface.ROTATION_180, + Surface.ROTATION_270 + ) + private val sensorManager: SensorManager = + context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val gyroSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + private var displayRotation: Int = displayRotationValues[0] + private var timestamp: Float = 0f + private var recenter: Boolean = false + private var recenterMul: Float = 1f + private var ease: Boolean = true + private var gyroSensorRegistered: Boolean = false + + // Speed per frame, based on 60FPS. + private var easingMul: Float = DEFAULT_EASING * BASE_FPS + + init { + onNewConfig() + } + + /** + * Starts listening for gyroscope events. + * (the rotation is also reset). + */ + fun start() { + gyroSensor?.let { + sensorManager.registerListener( + sensorEventListener, + it, + SensorManager.SENSOR_DELAY_GAME + ) + + gyroSensorRegistered = true + } + + if (gyroSensor == null) Log.w( + TAG, + "SensorManager could not find a default TYPE_GYROSCOPE sensor" + ) + } + + /** + * Stops listening for the gyroscope events. + * (the rotation is also reset). + */ + fun stop() { + if (gyroSensorRegistered) { + sensorManager.unregisterListener(sensorEventListener) + gyroSensorRegistered = false + } + } + + /** + * Resets the rotation values. + */ + fun resetValues() { + rotation = Vector2() + angles[0] = 0f + angles[1] = 0f + } + + /** + * Updates the output rotation (mostly it is used the update and ease the rotation value based + * on the [Gyro2dController.GyroConfig.easingSpeed] value). + * + * @param deltaSeconds the time in seconds elapsed since the last time + * [Gyro2dController.update] was called. + */ + fun update(deltaSeconds: Float) { + /* + * Ease if needed (specially to reduce movement variation which will allow us to use a + * smaller fps). + */ + rotation = if (ease) { + Vector2( + MathUtils.lerp(rotation.x, angles[0], easingMul * deltaSeconds), + MathUtils.lerp(rotation.y, angles[1], easingMul * deltaSeconds) + ) + } else { + Vector2(angles[0], angles[1]) + } + + isSettled = isCurrentlySettled() + isAlmostSettled = isNearlySettled() + } + + /** + * Call it to change how the gyro sensor is interpreted. This function is specially important + * when the display is not being presented in its default orientation (by default + * [Gyro2dController] will read the gyro values as if the device is in its default orientation). + * + * @param displayRotation The current display rotation. It can only be one of the following + * values: [Surface.ROTATION_0], [Surface.ROTATION_90], [Surface.ROTATION_180] or + * [Surface.ROTATION_270]. + */ + fun setDisplayRotation(displayRotation: Int) { + if (displayRotation !in displayRotationValues) { + throwDisplayRotationException(displayRotation) + } + + this.displayRotation = displayRotation + } + + /** + * Determine whether the gyro orientation is considered to be "settled" and unexpected to change + * in the near future. If a non-null [referenceRotation] is provided, then the gyro also won't + * be considered "settled" if the current or (expected) future state is too far from the + * reference. For example, clients can provide the value of our [rotation] at the time that they + * last presented that state to the user, to determine if that reference value is now too far + * behind. + */ + fun isCurrentlySettled(referenceRotation: Vector2 = rotation): Boolean = + (getErrorDistance(referenceRotation) < config.settledThreshold) + + /** Like [isCurrentlySettled], but with a wider tolerance. */ + fun isNearlySettled(referenceRotation: Vector2 = rotation): Boolean = + (getErrorDistance(referenceRotation) < config.almostSettledThreshold) + + /** + * Determine the amount of recent-or-expected angular rotation given our sensor values and + * easing state as documented for [isCurrentlySettled]. This is a signal for how frequently we + * should update based on gyro activity. + */ + private fun getErrorDistance(referenceRotation: Vector2 = rotation): Float { + val targetOrientation = Vector2(angles[0], angles[1]) + + // Have we now updated to a state far from the last one we presented? + val distanceFromReferenceToCurrent = referenceRotation.distanceTo(rotation) + + // Did our last frame have a long way to go to get to our current target? + val distanceFromReferenceToTarget = referenceRotation.distanceTo(targetOrientation) + + // Are we *currently* far from the target? Note we may often expect the current value to be + // somewhere *between* the target and the last-rendered rotation as each frame gets closer + // to the target, but it's actually possible for the target to move between updates such + // that the "current" value falls outside of the range. + val distanceFromCurrentToTarget = rotation.distanceTo(targetOrientation) + + return maxOf( + distanceFromReferenceToCurrent, + distanceFromReferenceToTarget, + distanceFromCurrentToTarget + ) + } + + private fun updateGyroRotation(event: SensorEvent) { + if (timestamp != 0f) { + val dT = (event.timestamp - timestamp) * NANOS_TO_S + // Adjust based on display rotation. + var axisX: Float = when (displayRotation) { + Surface.ROTATION_90 -> -event.values[1] + Surface.ROTATION_180 -> -event.values[0] + Surface.ROTATION_270 -> event.values[1] + else -> event.values[0] + } + + var axisY: Float = when (displayRotation) { + Surface.ROTATION_90 -> event.values[0] + Surface.ROTATION_180 -> -event.values[1] + Surface.ROTATION_270 -> -event.values[0] + else -> event.values[1] + } + + axisX *= RAD_TO_DEG * dT * config.intensity + axisY *= RAD_TO_DEG * dT * config.intensity + + angles[0] = updateAngle(angles[0], axisX, config.maxAngleRotation.x) + angles[1] = updateAngle(angles[1], axisY, config.maxAngleRotation.y) + } + + timestamp = event.timestamp.toFloat() + } + + private fun updateAngle(angle: Float, deltaAngle: Float, maxAngle: Float): Float { + // Adds incremental value. + var angleCombined = angle + deltaAngle + + // Clamps to maxAngleRotation x and maxAngleRotation y. + if (abs(angleCombined) > maxAngle) angleCombined = maxAngle * sign(angleCombined) + + // Re-centers to origin if needed. + if (recenter) angleCombined *= recenterMul + + return angleCombined + } + + private fun throwDisplayRotationException(displayRotation: Int) { + throw IllegalArgumentException( + "setDisplayRotation only accepts Surface.ROTATION_0 (0), " + + "Surface.ROTATION_90 (1), Surface.ROTATION_180 (2) or \n" + + "[Surface.ROTATION_270 (3); Instead the value was $displayRotation." + ) + } + + private fun onNewConfig() { + recenter = config.recenterSpeed > 0f + recenterMul = 1f - MathUtils.clamp(config.recenterSpeed, 0f, 1f) + ease = config.easingSpeed < 1f + easingMul = MathUtils.clamp(config.easingSpeed, 0f, 1f) * BASE_FPS + } + + /** + * Class that contains the config attributes for the gyro. + */ + data class GyroConfig( + /** + * Adjusts the maximum output rotation (in degrees) for both positive and negative angles, + * for each direction (x for the rotation around the X axis, y for the rotation + * around the Y axis). + * + * i.e. if [maxAngleRotation] = (2, 4), the output rotation would be inside + * ([-2º, 2º], [-4º, 4º]). + */ + val maxAngleRotation: Vector2 = Vector2(2f), + + /** + * Adjusts how much movement we need to apply to the device to make it rotate. This value + * multiplies the original rotation values; thus if the value is < 1f, we would need to + * rotate more the device than the actual rotation; if it is 1 it would be the default + * phone rotation; if it is > 1f it will magnify the rotation. + */ + val intensity: Float = 0.05f, + + /** + * Adjusts how much the end rotation is eased. This value can be from range [0, 1]. + * - When 0, the eased value won't change. + * - when 1, there isn't any easing. + */ + val easingSpeed: Float = 0.8f, + + /** + * How fast we want the rotation to recenter besides the gyro values. + * - When 0, it doesn't recenter. + * - when 1, it would make the rotation be in the center all the time. + */ + val recenterSpeed: Float = 0f, + + /** + * The minimum frame-over-frame delta required between gyroscope readings + * (by L2 distance in the rotation angles) in order to consider the device to be settled + * in a given animation frame. + */ + val settledThreshold: Float = 0.0005f, + + /** + * The minimum frame-over-frame delta required between the target orientation and the + * current orientation, in order to define if the orientation is almost settled. + */ + val almostSettledThreshold: Float = 0.01f + ) +} diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/interaction/HingeController.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/interaction/HingeController.kt new file mode 100644 index 0000000..57313e8 --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/interaction/HingeController.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2023 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.torus.utils.interaction + +import android.content.Context +import android.content.Context.SENSOR_SERVICE +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.os.Build +import androidx.annotation.FloatRange +import androidx.annotation.RequiresApi +import com.google.android.torus.utils.animation.EasingUtils +import kotlin.math.abs + +/** + * Class that listens to changes to the device hinge angle. + * This class can only be used on Android R or above + */ +@RequiresApi(Build.VERSION_CODES.R) +class HingeController(context: Context) { + + companion object { + + const val DEFAULT_EASING = 0.36f + const val ALMOST_SETTLED_THRESHOLD = 1f + const val SETTLED_THRESHOLD = .01f + } + + private var hingeAngleSensorValue: Float = 0f + var hingeAngle: Float = 0f + private set + + /** + * Adjusts how much the end hinge angle is eased. This value can be from range [0, 1]. + * - When 0, the eased value won't change. + * - when 1, there isn't any easing. + */ + @FloatRange(from = 0.0, to = 1.0) + var hingeEasingSpeed: Float = DEFAULT_EASING + + /** + * Defines if hinge angle is considered to be settled. + */ + var isSettled: Boolean = false + private set + + /** + * Defines whether the hinge animation is almost settled. + */ + var isAlmostSettled: Boolean = false + private set + + private var sensorManager: SensorManager = + context.getSystemService(SENSOR_SERVICE) as SensorManager + private var hingeAngleSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE) + private var sensorListener: SensorEventListener? = object : SensorEventListener { + override fun onSensorChanged(event: SensorEvent) { + hingeAngleSensorValue = event.values[0] + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + } + } + + /** + * Starts listening for hinge events. + */ + fun start() { + hingeAngleSensor?.let { + sensorManager.registerListener( + sensorListener, + it, + SensorManager.SENSOR_DELAY_NORMAL + ) + } + } + + /** + * Stops listening for hinge events. + */ + fun stop() { + hingeAngleSensor?.let { + sensorManager.unregisterListener(sensorListener, it) + } + } + + /** + * Updates the output hinge angle using easing settings. + * + * @param deltaSeconds the time in seconds elapsed since the last time + * [HingeController.update] was called. + */ + fun update(deltaSeconds: Float) { + /* + * Ease if needed (specially to reduce movement variation which will allow us to use a + * smaller fps). + */ + hingeAngle = EasingUtils.calculateEasing( + hingeAngle, + hingeAngleSensorValue, + hingeEasingSpeed, + deltaSeconds + ) + + isSettled = isCurrentlySettled() + isAlmostSettled = isNearlySettled() + } + + /** + * Determine the amount of recent-or-expected angular rotation given our sensor values and + * easing state as documented for [isCurrentlySettled]. This is a signal for how frequently we + * should update based on hinge activity. + */ + private fun getErrorDistance(referenceHingeAngle: Float = hingeAngle): Float { + // Have we now updated to a state far from the last one we presented? + val distanceFromReferenceToCurrent = abs(referenceHingeAngle - hingeAngle) + + // Did our last frame have a long way to go to get to our current target? + val distanceFromReferenceToTarget = abs(referenceHingeAngle - hingeAngleSensorValue) + + // Are we *currently* far from the target? Note we may often expect the current value to be + // somewhere *between* the target and the last-rendered angle as each frame gets closer + // to the target, but it's actually possible for the target to move between updates such + // that the "current" value falls outside of the range. + val distanceFromCurrentToTarget = abs(hingeAngleSensorValue - hingeAngle) + + return maxOf( + distanceFromReferenceToCurrent, + distanceFromReferenceToTarget, + distanceFromCurrentToTarget + ) + } + + /** + * Determine whether the hinge angle is considered to be "settled" and unexpected to change + * in the near future. + */ + fun isCurrentlySettled(referenceHingeAngle: Float = hingeAngle): Boolean = + (getErrorDistance(referenceHingeAngle) < SETTLED_THRESHOLD) + + /** Like [isCurrentlySettled], but with a wider tolerance. */ + fun isNearlySettled(referenceHingeAngle: Float = hingeAngle): Boolean = + (getErrorDistance(referenceHingeAngle) < ALMOST_SETTLED_THRESHOLD) +} diff --git a/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/wallpaper/WallpaperUtils.kt b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/wallpaper/WallpaperUtils.kt new file mode 100644 index 0000000..012768d --- /dev/null +++ b/toruslib/torus-utils/src/main/java/com/google/android/torus/utils/wallpaper/WallpaperUtils.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 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.torus.utils.wallpaper + +import android.app.WallpaperColors +import android.graphics.Color +import android.os.Build + +/** Creates some utils for wallpapers. */ +object WallpaperUtils { + /** + * Returns a [WallpaperColors] with the color provided and launcher using black text + * if [darkText] is true. + * + * @param primaryColor Primary color. + * @param secondaryColor Secondary color. + * @param tertiaryColor Tertiary color. + * @param darkText If the launcher should use dark text (It won't work for SDK < 31 (S)). + * + * @return the wallpaper color with the color hints (if possible). + */ + @JvmStatic + fun getWallpaperColors( + primaryColor: Color, + secondaryColor: Color, + tertiaryColor: Color, + darkText: Boolean = false + ): WallpaperColors { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && darkText) { + WallpaperColors( + primaryColor, + secondaryColor, + tertiaryColor, + WallpaperColors.HINT_SUPPORTS_DARK_TEXT or WallpaperColors.HINT_SUPPORTS_DARK_THEME + ) + } else { + WallpaperColors( + primaryColor, + secondaryColor, + tertiaryColor, + ) + } + } +} diff --git a/toruslib/torus-wallpaper-settings/build.gradle b/toruslib/torus-wallpaper-settings/build.gradle new file mode 100644 index 0000000..e6e0ee4 --- /dev/null +++ b/toruslib/torus-wallpaper-settings/build.gradle @@ -0,0 +1,19 @@ +// Copyright (C) 2023 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. + +dependencies { + implementation project(':torus-utils') + implementation "androidx.slice:slice-builders:$versions.androidXLib" + implementation "androidx.slice:slice-core:$versions.androidXLib" +} \ No newline at end of file diff --git a/toruslib/torus-wallpaper-settings/src/main/AndroidManifest.xml b/toruslib/torus-wallpaper-settings/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3c68a96 --- /dev/null +++ b/toruslib/torus-wallpaper-settings/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + diff --git a/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/BaseSliceConfigProvider.kt b/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/BaseSliceConfigProvider.kt new file mode 100755 index 0000000..5b9aa11 --- /dev/null +++ b/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/BaseSliceConfigProvider.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 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.torus.settings.inlinecontrol + +import android.Manifest +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import android.net.Uri +import androidx.slice.SliceProvider +import androidx.slice.builders.ListBuilder +import com.google.android.torus.settings.storage.CustomizedSharedPreferences + +/** + * BaseSliceConfigProvider is the base class for configuration wallpaper Slices. + * It can be extended and overridden the [uri], [onBindSlice], and [onConfigChange] for providing + * different Slice UI. + */ +abstract class BaseSliceConfigProvider : SliceProvider(Manifest.permission.BIND_WALLPAPER) { + companion object { + const val EXTRA_WALLPAPER_URI = "extra_wallpaper_uri" + } + + protected abstract val sharedPrefKey: String + abstract val uriStringId: Int + abstract fun onConfigChange() + + protected lateinit var preferences: CustomizedSharedPreferences + protected lateinit var listBuilder: ListBuilder + private lateinit var sharedPreferenceListener: OnSharedPreferenceChangeListener + private lateinit var uri: Uri + + override fun onCreateSliceProvider(): Boolean { + context?.let { context -> + uri = Uri.parse(context.getString(uriStringId)) + preferences = CustomizedSharedPreferences(context, sharedPrefKey) + sharedPreferenceListener = OnSharedPreferenceChangeListener { _, _ -> + preferences.load(true) + onConfigChange() + updateSlice() + } + preferences.register(sharedPreferenceListener) + preferences.load(true) + onConfigChange() + return true + } ?: return false + } + + private fun updateSlice() { + context?.contentResolver?.notifyChange(uri, null) + } +} diff --git a/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/ColorChipsRowBuilder.kt b/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/ColorChipsRowBuilder.kt new file mode 100755 index 0000000..7f5cd29 --- /dev/null +++ b/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/ColorChipsRowBuilder.kt @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2023 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.torus.settings.inlinecontrol + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.text.TextUtils +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.IconCompat +import androidx.slice.builders.GridRowBuilder +import androidx.slice.builders.GridRowBuilder.CellBuilder +import androidx.slice.builders.ListBuilder +import androidx.slice.builders.SliceAction +import com.google.android.torus.R +import com.google.android.torus.settings.inlinecontrol.BaseSliceConfigProvider.Companion.EXTRA_WALLPAPER_URI +import kotlin.math.max + +/** + * ColorChipsRowBuilder is used to construct and hold Slice rows data and color configurations + * for wallpapers that are configurable using the provided params in {@link #create()} method + */ +object ColorChipsRowBuilder { + var ACTION_PRIMARY = ".action.PRIMARY" + var ACTION_SET_COLOR = ".action.SET_COLOR" + var EXTRA_COLOR_INDEX = "extra_color_index" + var DEFAULT_COLOR_INDEX = 0 + + @JvmOverloads + fun create( + context: Context, + colorOptions: Array, + selectedItem: Int, + title: CharSequence? = null, + minSpaces: Int = 0, // disabled by default + wallpaperUriString: String? = "", + wallpaperName: String, + zoomOnSelection: Boolean + ): GridRowBuilder? { + val packageName = context.packageName + if (TextUtils.isEmpty(packageName)) return null + + val res = context.resources ?: return null + val titleAdjusted = title ?: res.getText(R.string.color_chips_title) + + val iconHeight = res.getDimensionPixelSize(R.dimen.slice_icon_height) + val iconWidth = res.getDimensionPixelSize(R.dimen.slice_icon_width) + val gridRowBuilder = GridRowBuilder() + val bmpEmpty = Bitmap.createBitmap(iconWidth, iconHeight, Bitmap.Config.ARGB_8888) + val emptyCell = CellBuilder() + .addImage(IconCompat.createWithBitmap(bmpEmpty), ListBuilder.SMALL_IMAGE) + val primaryAction = SliceAction.create( + PendingIntent.getBroadcast( + context, 0, + Intent("$packageName.$wallpaperName$ACTION_PRIMARY").setPackage(packageName) + .putExtra(EXTRA_COLOR_INDEX, 0), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ), + IconCompat.createWithBitmap(bmpEmpty), + ListBuilder.SMALL_IMAGE, "" + ) + + gridRowBuilder.primaryAction = primaryAction + + // Content description for this grid row. + gridRowBuilder.setContentDescription(titleAdjusted) + + for (cellIndex in 0..max(colorOptions.size - 1, minSpaces - 1)) { + if (cellIndex < colorOptions.size) { + // Add color option + gridRowBuilder.addCell( + makeColorOption( + colorOptions, + cellIndex, + selectedItem, + zoomOnSelection, + res, + context, + packageName, + wallpaperUriString, + wallpaperName + ) + ) + } else { + // Add empty cell for unused spaces + gridRowBuilder.addCell(emptyCell) + } + } + return gridRowBuilder + } + + private fun makeColorOption( + colorOptions: Array, + cellIndex: Int, + selectedItem: Int, + zoomOnSelection: Boolean, + res: Resources, + context: Context, + packageName: String, + wallpaperUriString: String?, + wallpaperName: String + ) = CellBuilder() + .addImage( + getIcon( + option = colorOptions[cellIndex], + selected = cellIndex == selectedItem, + zoomOnSelection = zoomOnSelection, + res = res + ), + ListBuilder.SMALL_IMAGE + ) + .setContentIntent( + PendingIntent.getBroadcast( + context, cellIndex, + Intent("$packageName.$wallpaperName$ACTION_SET_COLOR") + .setPackage(packageName) + .putExtra( + EXTRA_COLOR_INDEX, + cellIndex + ).putExtra(EXTRA_WALLPAPER_URI, wallpaperUriString), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + ) + .setContentDescription(res.getText(colorOptions[cellIndex].description)) + + private fun getIcon( + option: ColorOption, + selected: Boolean, + zoomOnSelection: Boolean, + res: Resources + ): IconCompat { + val iconHeight = res.getDimensionPixelSize(R.dimen.slice_icon_height) + val iconWidth = res.getDimensionPixelSize(R.dimen.slice_icon_width) + val colorChipHeight = res.getDimensionPixelSize(R.dimen.color_chips_height) + val colorChipPenWidth = res.getDimensionPixelSize(R.dimen.color_chips_pen_width) + val iconOnSize = res.getDimensionPixelSize(R.dimen.torus_slice_vector_icon_on_size) + val iconOffSize = if (zoomOnSelection) { + res.getDimensionPixelSize(R.dimen.torus_slice_vector_icon_off_size) + } else { + res.getDimensionPixelSize(R.dimen.torus_slice_vector_icon_on_size) + } + val bmp = Bitmap.createBitmap(iconWidth, iconHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bmp) + val paint = Paint() + if (option.drawable == -1) { + if (selected) { + paint.style = Paint.Style.FILL_AND_STROKE + } else { + paint.style = Paint.Style.STROKE + } + paint.strokeWidth = colorChipPenWidth.toFloat() + paint.color = option.value + paint.isAntiAlias = true + canvas.drawCircle( + (iconWidth / 2).toFloat(), + (iconHeight / 2).toFloat(), + ((colorChipHeight - colorChipPenWidth) / 2).toFloat(), + paint + ) + } else { + val drawableSize = if (selected) iconOnSize else iconOffSize + + val drawable = if (selected && option.drawableSelected != 1) { + ResourcesCompat.getDrawable(res, option.drawableSelected, null) + } else { + ResourcesCompat.getDrawable(res, option.drawable, null) + } + drawable?.setBounds(0, 0, drawableSize, drawableSize) + canvas.translate( + ((iconWidth - drawableSize) / 2).toFloat(), + ((iconHeight - drawableSize) / 2).toFloat() + ) + drawable?.draw(canvas) + } + return IconCompat.createWithBitmap(bmp) + } + + /** + * A color option to present in an inline control menu + */ + open class ColorOption { + @ColorInt + var value: Int + + @StringRes + val description: Int + + @DrawableRes + val drawable: Int + + @DrawableRes + val drawableSelected: Int + + constructor( + @StringRes description: Int, + @DrawableRes drawable: Int, + @DrawableRes drawableSelected: Int + ) { + this.description = description + this.value = -1 + this.drawable = drawable + this.drawableSelected = drawableSelected + } + + constructor(@StringRes description: Int, @ColorInt value: Int) { + this.description = description + this.value = value + drawable = -1 + drawableSelected = -1 + } + } +} diff --git a/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/InputRangeRowBuilder.kt b/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/InputRangeRowBuilder.kt new file mode 100644 index 0000000..db9443e --- /dev/null +++ b/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/InputRangeRowBuilder.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 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.torus.settings.inlinecontrol + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.slice.builders.ListBuilder +import com.google.android.torus.settings.inlinecontrol.BaseSliceConfigProvider.Companion.EXTRA_WALLPAPER_URI + +/** + * InputRangeRowBuilder is used to construct and hold Slice rows data, color options, + * and range value for wallpaper configurations that have an input range slider + */ +object InputRangeRowBuilder { + var ACTION_SET_RANGE = ".action.SET_RANGE" + var DEFAULT_RANGE_VALUE = 40 + + fun createInputRangeBuilder( + context: Context, + rangeIntent: Intent, + currentRangeValue: Int, + minRangeValue: Int, + maxRangeValue: Int, + title: String + ): ListBuilder.InputRangeBuilder { + val rangePendingIntent: PendingIntent = PendingIntent.getBroadcast( + context, + 0, + rangeIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + + return ListBuilder.InputRangeBuilder() + .setTitle(title) + .setInputAction(rangePendingIntent) + .setMin(minRangeValue) + .setMax(maxRangeValue) + .setValue(currentRangeValue) + } + + fun getInputRangeRowIntent( + context: Context, + wallpaper: Uri, + wallpaperName: String + ): Intent { + val packageName = context.packageName + return Intent("$packageName.$wallpaperName$ACTION_SET_RANGE") + .setPackage(packageName) + .putExtra( + EXTRA_WALLPAPER_URI, + wallpaper.toString() + ) + } +} diff --git a/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/SingleSelectionRowConfigProvider.kt b/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/SingleSelectionRowConfigProvider.kt new file mode 100644 index 0000000..c3cccc7 --- /dev/null +++ b/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/SingleSelectionRowConfigProvider.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 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.torus.settings.inlinecontrol + +import android.content.Context +import android.net.Uri +import androidx.annotation.StringRes +import androidx.slice.Slice +import androidx.slice.builders.ListBuilder +import com.google.android.torus.R + +/** + * SingleSelectionRowConfigProvider provides the most common used Slice UI among the wallpapers. + * It will get ColorOptions from wallpaper engines and build a row of color chips for user + * to choose. It can be extended and overridden the onBindSlice for providing different Slice UI. + */ +open class SingleSelectionRowConfigProvider( + val colorOptions: Array?, + override val sharedPrefKey: String, + @StringRes override val uriStringId: Int, + @StringRes open val wallpaperNameResId: Int, + @StringRes val sliceTitleStringId: Int = R.string.color_chips_title, + val minSpaces: Int = MIN_SPACES, + private val zoomOnSelection: Boolean = false +) : BaseSliceConfigProvider() { + companion object { + private const val MIN_SPACES = 5 + const val PREF_COLOR_INDEX = "COlOR_INDEX" + } + + protected var colorIndex = 0 + + override fun onBindSlice(sliceUri: Uri): Slice? { + context?.let { context -> + val title: CharSequence = context.getString(wallpaperNameResId) + val subtitleResId: Int = + colorOptions?.get(colorIndex)?.description ?: sliceTitleStringId + listBuilder = ListBuilder(context, sliceUri, ListBuilder.INFINITY) + buildColorChips( + context, + title, + context.getString(subtitleResId), + sliceUri + ) + + return listBuilder.build() + } ?: run { + return null + } + } + + override fun onConfigChange() { + colorIndex = preferences.getInt( + PREF_COLOR_INDEX, + ColorChipsRowBuilder.DEFAULT_COLOR_INDEX + ) + } + + protected fun buildColorChips( + context: Context, + title: CharSequence, + subtitle: CharSequence, + sliceUri: Uri + ) { + val gridRowBuilder = colorOptions?.let { + ColorChipsRowBuilder.create( + context = context, + colorOptions = it, + selectedItem = colorIndex, + title = title, + minSpaces = minSpaces, + wallpaperUriString = sliceUri.toString(), + wallpaperName = context.getString(wallpaperNameResId), + zoomOnSelection = zoomOnSelection + ) + } + gridRowBuilder?.let { + listBuilder + .setHeader( + ListBuilder.HeaderBuilder() + .setTitle(title) + .setSubtitle(subtitle) + ) + .addGridRow(it) + } + } +} diff --git a/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/SliceConfigController.kt b/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/SliceConfigController.kt new file mode 100644 index 0000000..b80f92b --- /dev/null +++ b/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/inlinecontrol/SliceConfigController.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 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.torus.settings.inlinecontrol + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.google.android.torus.utils.broadcast.BroadcastEventController + +/** + * SliceConfigController registers a BroadcastReceiver that listens to + * the intent filter provided by an implementation of [BaseSliceConfigProvider]. + * Forwards received broadcasts to be handled by a [SliceConfigController.SliceConfigListener]. + */ +class SliceConfigController( + context: Context, + private val sliceIntentFilter: IntentFilter, + private val sliceConfigListener: SliceConfigListener +) : BroadcastEventController(context) { + + override fun initResources(): Boolean { + return true + } + + override fun onBroadcastReceived(context: Context, intent: Intent, action: String) { + intent?.let { sliceConfigListener.onSliceConfig(it) } + } + + override fun onRegister(fire: Boolean): IntentFilter { + return sliceIntentFilter + } + + override fun onUnregister() {} + + interface SliceConfigListener { + fun onSliceConfig(intent: Intent) + } +} diff --git a/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/storage/CustomizedSharedPreferences.kt b/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/storage/CustomizedSharedPreferences.kt new file mode 100755 index 0000000..f832a1d --- /dev/null +++ b/toruslib/torus-wallpaper-settings/src/main/java/com/google/android/torus/settings/storage/CustomizedSharedPreferences.kt @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2023 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.torus.settings.storage + +import android.content.Context +import android.content.SharedPreferences +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import android.os.Bundle +import android.util.Log +import androidx.annotation.GuardedBy +import org.json.JSONException +import org.json.JSONObject + +/** + * This class provide APIs to save and load a bundle of values that may map to + * customization (inline control) settings. Each instance will have two set of values, one for + * normal and one for preview mode. + * Currently supported value types are boolean, integer, float and string. + */ +class CustomizedSharedPreferences(context: Context, name: String) { + companion object { + private const val TAG = "CustomizedSharedPref" + private const val DEBUG = false + private const val SUFFIX_PREVIEW = "_preview" + const val PREF_FILENAME = "inline_control" + + /** + * Convert a JSONObject to Bundle. + * + * @param json a JSONObject + * @return a Bundle object + */ + private fun jsonToBundle(json: JSONObject): Bundle { + val bundle = Bundle() + val it = json.keys() + while (it.hasNext()) { + val key = it.next() + try { + when (val obj = json[key]) { + is Boolean -> { + bundle.putBoolean(key, obj) + } + is Int -> { + bundle.putInt(key, obj) + } + is Double -> { + // Only support Float, but JSONObject save Float as Double, + // So we convert Double to Float here. + bundle.putFloat(key, obj.toFloat()) + } + is CharSequence -> { + bundle.putString(key, obj as String) + } + } + } catch (e: JSONException) { + Log.w(TAG, "JSONObject get fail for $key") + } + } + return bundle + } + + /** + * Convert a Bundle to JSONObject. + * + * @param bundle a Bundle to convert + * @return a JsonObject object + */ + private fun bundleToJson(bundle: Bundle): JSONObject { + val jsonObj = JSONObject() + for (key in bundle.keySet()) { + try { + jsonObj.put(key, bundle[key]) + } catch (e: JSONException) { + if (DEBUG) { + e.printStackTrace() + } + } + } + + return jsonObj + } + } + + private val preferences: SharedPreferences + private val wallpaperConfigName: String + private val lock = Any() + + @GuardedBy("lock") + private var bundle = Bundle() + + /** + * Create an instance with a config name, the name should be unique among all wallpapers. + */ + init { + // Use createDeviceProtectedStorageContext to support direct boot. + // The shared preference file will be saved under: + // /data/user_de/0/com.google.pixel.livewallpapers/shared_prefs + val secureContext = context.createDeviceProtectedStorageContext() + preferences = secureContext.getSharedPreferences(PREF_FILENAME, Context.MODE_MULTI_PROCESS) + wallpaperConfigName = name + } + + /** + * Save the bundle keys and values that have put in this object. + * + * @param isPreview Select save keys and values as normal set or preview set. + */ + fun save(isPreview: Boolean) { + lateinit var jsonObj: JSONObject + synchronized(lock) { jsonObj = bundleToJson(bundle) } + val editor = preferences.edit() + val keySuffix = if (isPreview) SUFFIX_PREVIEW else "" + editor.putString(wallpaperConfigName + keySuffix, jsonObj.toString()) + editor.apply() + } + + /** + * Load the bundle keys and values that stored in shared preferences file. + * + * @param isPreview Select load keys and values from normal set or preview set. + */ + fun load(isPreview: Boolean) { + val keySuffix = if (isPreview) SUFFIX_PREVIEW else "" + val jsonStr = preferences.getString(wallpaperConfigName + keySuffix, "") + try { + val json = JSONObject(jsonStr) + synchronized(lock) { bundle = jsonToBundle(json) } + } catch (e: JSONException) { + if (DEBUG) { + Log.w(TAG, "JSONObject creation failed for \n$jsonStr") + e.printStackTrace() + } + } + } + + /** + * Inserts a boolean value into the bundle in CustomizedSharedPreferences, replacing any + * existing value for the given key. + * + * @param key a String key + * @param value a boolean + */ + fun putBoolean(key: String, value: Boolean) { + synchronized(lock) { bundle.putBoolean(key, value) } + } + + /** + * Inserts an int value into the bundle in CustomizedSharedPreferences, replacing any existing + * value for the given key. + * + * @param key a String key + * @param value an int + */ + fun putInt(key: String, value: Int) { + synchronized(lock) { bundle.putInt(key, value) } + } + + /** + * Inserts a float value into the bundle in CustomizedSharedPreferences, replacing any existing + * value for the given key. + * + * @param key a String key + * @param value a float + */ + fun putFloat(key: String, value: Float) { + synchronized(lock) { bundle.putFloat(key, value) } + } + + /** + * Inserts a String value into the bundle in CustomizedSharedPreferences, replacing any existing + * value for the given key. + * + * @param key a String key + * @param value a String + */ + fun putString(key: String, value: String) { + synchronized(lock) { bundle.putString(key, value) } + } + + /** + * Return the value associated with the given key from currently loaded shared preference set, + * or defaultValue if no mapping of the desired value of type Boolean exists for the given key. + * + * @param key a String key + * @param defaultValue Value to return if key does not exist + */ + fun getBoolean(key: String, defaultValue: Boolean): Boolean { + synchronized(lock) { return bundle.getBoolean(key, defaultValue) } + } + + /** + * Return the value associated with the given key from currently loaded shared preference set, + * or defaultValue if no mapping of the desired value of type Int exists for the given key. + * + * @param key a String key + * @param defaultValue Value to return if key does not exist + */ + fun getInt(key: String, defaultValue: Int): Int { + synchronized(lock) { return bundle.getInt(key, defaultValue) } + } + + /** + * Return the value associated with the given key from currently loaded shared preference set, + * or defaultValue if no mapping of the desired value of type Float exists for the given key. + * + * @param key a String key + * @param defaultValue Value to return if key does not exist + */ + fun getFloat(key: String, defaultValue: Float): Float { + synchronized(lock) { return bundle.getFloat(key, defaultValue) } + } + + /** + * Return the value associated with the given key from currently loaded shared preference set, + * or defaultValue if no mapping of the desired value of type String exists for the given key. + * + * @param key a String key + * @param defaultValue Value to return if key does not exist + */ + fun getString(key: String, defaultValue: String?): String { + synchronized(lock) { return bundle.getString(key, defaultValue) } + } + + /** + * Remove any entry with the given key. + * + * @param key a String key + */ + fun remove(key: String) { + synchronized(lock) { bundle.remove(key) } + } + + fun register(listener: OnSharedPreferenceChangeListener?) { + preferences.registerOnSharedPreferenceChangeListener(listener) + } +} diff --git a/toruslib/torus-wallpaper-settings/src/main/res/values/dimens.xml b/toruslib/torus-wallpaper-settings/src/main/res/values/dimens.xml new file mode 100755 index 0000000..9aa1416 --- /dev/null +++ b/toruslib/torus-wallpaper-settings/src/main/res/values/dimens.xml @@ -0,0 +1,11 @@ + + + + 24dp + 48dp + 48dp + 4dp + + 48dp + 24dp + diff --git a/toruslib/torus-wallpaper-settings/src/main/res/values/strings.xml b/toruslib/torus-wallpaper-settings/src/main/res/values/strings.xml new file mode 100644 index 0000000..427ade5 --- /dev/null +++ b/toruslib/torus-wallpaper-settings/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Choose color + \ No newline at end of file -- cgit v1.2.3 From 9be4403dcdd5824fedc66c1f6dbd1709e091744f Mon Sep 17 00:00:00 2001 From: Stefan Andonian Date: Mon, 18 Sep 2023 20:46:57 +0000 Subject: Fix NPE in ViewCapture test helper Bug: 300947047 Test: NPE stopped occuring in tests. Change-Id: I662d3edb937e1949187b0cb96fd840ad8471f1f1 --- viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java | 1 + 1 file changed, 1 insertion(+) diff --git a/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java index 507c5a8..d366c0a 100644 --- a/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java +++ b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java @@ -382,6 +382,7 @@ public abstract class ViewCapture { } void attachToRoot() { + if (mRoot == null) return; mIsActive = true; if (mRoot.isAttachedToWindow()) { safelyEnableOnDrawListener(); -- cgit v1.2.3 From d4d9e6aff0ca83b7f83b124bfe32784d935abc66 Mon Sep 17 00:00:00 2001 From: Yein Jo Date: Fri, 15 Sep 2023 23:31:22 +0000 Subject: Copy over WeatherEffects. - Need to separate prod and debug builds. - ViewBinding is removed as it's not supported by Soong build. - Interactor was moved to the correct pacakge location. - Added some more comments & javadocs. - Removed debug assets due to copyright. Bug: 290939683 Bug: 300991599 Test: mmm weathereffects, gradle build Change-Id: Ibcbbbe1f8a8ae42c310f04bb4b3e67b8409da069 --- toruslib/Android.bp | 1 + toruslib/build.gradle | 2 + toruslib/lib-torus/build.gradle | 6 +- toruslib/lib-torus/gradle.properties | 14 ++ .../android/torus/core/wallpaper/LiveWallpaper.kt | 8 +- weathereffects/Android.bp | 76 +++++++ weathereffects/AndroidManifest.xml | 53 +++++ .../assets/shaders/color_grading_lut.agsl | 80 +++++++ weathereffects/assets/shaders/constants.agsl | 19 ++ weathereffects/assets/shaders/fog_effect.agsl | 75 +++++++ weathereffects/assets/shaders/glass_rain.agsl | 147 +++++++++++++ weathereffects/assets/shaders/rain.agsl | 96 +++++++++ weathereffects/assets/shaders/rain_effect.agsl | 185 +++++++++++++++++ weathereffects/assets/shaders/simplex_noise.agsl | 110 ++++++++++ weathereffects/assets/shaders/snow.agsl | 114 ++++++++++ weathereffects/assets/shaders/snow_effect.agsl | 88 ++++++++ weathereffects/assets/shaders/utils.agsl | 46 +++++ .../assets/textures/lut_rain_and_fog.png | Bin 0 -> 24530 bytes weathereffects/build.gradle | 161 +++++++++++++++ weathereffects/debug/AndroidManifest.xml | 41 ++++ weathereffects/debug/assets/test-background.png | Bin 0 -> 62714 bytes weathereffects/debug/assets/test-foreground.png | Bin 0 -> 55354 bytes .../debug/res/drawable/ic_baseline_check_24.xml | 25 +++ .../res/drawable/ic_baseline_image_search_24.xml | 22 ++ weathereffects/debug/res/layout/debug_activity.xml | 109 ++++++++++ weathereffects/debug/res/values/colors.xml | 21 ++ weathereffects/debug/res/values/strings.xml | 27 +++ weathereffects/debug/res/values/themes.xml | 23 +++ .../WallpaperEffectsDebugActivity.kt | 230 +++++++++++++++++++++ .../WallpaperEffectsDebugApplication.kt | 37 ++++ .../dagger/DebugApplicationComponent.kt | 33 +++ weathereffects/gradle.properties | 18 ++ weathereffects/includes.gradle | 18 ++ .../res/drawable/ic_launcher_background.xml | 185 +++++++++++++++++ .../res/drawable/ic_launcher_foreground.xml | 45 ++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 20 ++ weathereffects/res/values/strings.xml | 21 ++ weathereffects/res/xml/weather_wallpaper.xml | 21 ++ weathereffects/settings.gradle | 17 ++ .../wallpaper/weathereffects/WeatherEffect.kt | 53 +++++ .../wallpaper/weathereffects/WeatherEngine.kt | 160 ++++++++++++++ .../weathereffects/WeatherWallpaperService.kt | 43 ++++ .../weathereffects/dagger/ApplicationComponent.kt | 24 +++ .../weathereffects/dagger/DependencyProvider.kt | 65 ++++++ .../wallpaper/weathereffects/dagger/Qualifiers.kt | 35 ++++ .../data/repository/WallpaperFileUtils.kt | 136 ++++++++++++ .../data/repository/WeatherEffectsRepository.kt | 128 ++++++++++++ .../domain/WeatherEffectsInteractor.kt | 37 ++++ .../wallpaper/weathereffects/fog/FogEffect.kt | 155 ++++++++++++++ .../weathereffects/fog/FogEffectConfig.kt | 68 ++++++ .../wallpaper/weathereffects/none/NoEffect.kt | 67 ++++++ .../provider/WallpaperInfoContract.kt | 90 ++++++++ .../provider/WeatherEffectsContentProvider.kt | 115 +++++++++++ .../wallpaper/weathereffects/rain/RainEffect.kt | 140 +++++++++++++ .../weathereffects/rain/RainEffectConfig.kt | 71 +++++++ .../shared/model/WallpaperFileModel.kt | 46 +++++ .../wallpaper/weathereffects/snow/SnowEffect.kt | 129 ++++++++++++ .../weathereffects/snow/SnowEffectConfig.kt | 71 +++++++ .../weathereffects/utils/GraphicsUtils.kt | 114 ++++++++++ .../wallpaper/weathereffects/utils/ImageCrop.kt | 73 +++++++ .../wallpaper/weathereffects/utils/MatrixUtils.kt | 41 ++++ .../provider/WeatherEffectsContentProviderTest.kt | 114 ++++++++++ 62 files changed, 4162 insertions(+), 7 deletions(-) create mode 100644 weathereffects/Android.bp create mode 100644 weathereffects/AndroidManifest.xml create mode 100644 weathereffects/assets/shaders/color_grading_lut.agsl create mode 100644 weathereffects/assets/shaders/constants.agsl create mode 100644 weathereffects/assets/shaders/fog_effect.agsl create mode 100644 weathereffects/assets/shaders/glass_rain.agsl create mode 100644 weathereffects/assets/shaders/rain.agsl create mode 100644 weathereffects/assets/shaders/rain_effect.agsl create mode 100644 weathereffects/assets/shaders/simplex_noise.agsl create mode 100644 weathereffects/assets/shaders/snow.agsl create mode 100644 weathereffects/assets/shaders/snow_effect.agsl create mode 100644 weathereffects/assets/shaders/utils.agsl create mode 100644 weathereffects/assets/textures/lut_rain_and_fog.png create mode 100644 weathereffects/build.gradle create mode 100644 weathereffects/debug/AndroidManifest.xml create mode 100644 weathereffects/debug/assets/test-background.png create mode 100644 weathereffects/debug/assets/test-foreground.png create mode 100644 weathereffects/debug/res/drawable/ic_baseline_check_24.xml create mode 100644 weathereffects/debug/res/drawable/ic_baseline_image_search_24.xml create mode 100644 weathereffects/debug/res/layout/debug_activity.xml create mode 100644 weathereffects/debug/res/values/colors.xml create mode 100644 weathereffects/debug/res/values/strings.xml create mode 100644 weathereffects/debug/res/values/themes.xml create mode 100644 weathereffects/debug/src/com/google/android/wallpaper/weathereffects/WallpaperEffectsDebugActivity.kt create mode 100644 weathereffects/debug/src/com/google/android/wallpaper/weathereffects/WallpaperEffectsDebugApplication.kt create mode 100644 weathereffects/debug/src/com/google/android/wallpaper/weathereffects/dagger/DebugApplicationComponent.kt create mode 100644 weathereffects/gradle.properties create mode 100644 weathereffects/includes.gradle create mode 100644 weathereffects/res/drawable/ic_launcher_background.xml create mode 100644 weathereffects/res/drawable/ic_launcher_foreground.xml create mode 100644 weathereffects/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 weathereffects/res/values/strings.xml create mode 100644 weathereffects/res/xml/weather_wallpaper.xml create mode 100644 weathereffects/settings.gradle create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/WeatherEffect.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/WeatherEngine.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/WeatherWallpaperService.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/dagger/ApplicationComponent.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/dagger/DependencyProvider.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/dagger/Qualifiers.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/data/repository/WallpaperFileUtils.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/data/repository/WeatherEffectsRepository.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/domain/WeatherEffectsInteractor.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/fog/FogEffect.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/fog/FogEffectConfig.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/none/NoEffect.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/provider/WallpaperInfoContract.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/provider/WeatherEffectsContentProvider.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/rain/RainEffect.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/rain/RainEffectConfig.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/shared/model/WallpaperFileModel.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/snow/SnowEffect.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/snow/SnowEffectConfig.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/utils/GraphicsUtils.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/utils/ImageCrop.kt create mode 100644 weathereffects/src/com/google/android/wallpaper/weathereffects/utils/MatrixUtils.kt create mode 100644 weathereffects/tests/src/com/google/android/wallpaper/weathereffects/provider/WeatherEffectsContentProviderTest.kt diff --git a/toruslib/Android.bp b/toruslib/Android.bp index 94763b7..e42f205 100644 --- a/toruslib/Android.bp +++ b/toruslib/Android.bp @@ -47,6 +47,7 @@ android_library { optimize: { enabled: true, }, + min_sdk_version: "31", sdk_version: "system_current", } diff --git a/toruslib/build.gradle b/toruslib/build.gradle index ecbe754..f97cb03 100644 --- a/toruslib/build.gradle +++ b/toruslib/build.gradle @@ -53,6 +53,8 @@ subprojects { apply plugin: 'kotlin-android' android { + namespace "com.google.android.torus" + compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools diff --git a/toruslib/lib-torus/build.gradle b/toruslib/lib-torus/build.gradle index 7fd8263..106563b 100644 --- a/toruslib/lib-torus/build.gradle +++ b/toruslib/lib-torus/build.gradle @@ -31,12 +31,12 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { diff --git a/toruslib/lib-torus/gradle.properties b/toruslib/lib-torus/gradle.properties index 7b00076..1605871 100644 --- a/toruslib/lib-torus/gradle.properties +++ b/toruslib/lib-torus/gradle.properties @@ -1,3 +1,17 @@ +# Copyright (C) 2023 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. + # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: diff --git a/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt b/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt index c69bff8..9a5d96e 100644 --- a/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt +++ b/toruslib/torus-core/src/main/java/com/google/android/torus/core/wallpaper/LiveWallpaper.kt @@ -24,7 +24,6 @@ import android.content.IntentFilter import android.content.res.Configuration import android.graphics.PixelFormat import android.os.Build -import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE import android.os.Bundle import android.service.wallpaper.WallpaperService import android.view.MotionEvent @@ -209,10 +208,11 @@ abstract class LiveWallpaper : WallpaperService() { fun getEngineSurfaceHolder(): SurfaceHolder? = this.wallpaperServiceEngine?.surfaceHolder /** Returns the wallpaper flags indicating which screen this Engine is rendering to. */ - @RequiresApi(UPSIDE_DOWN_CAKE) fun getWallpaperFlags(): Int { - this.wallpaperServiceEngine?.let { - return it.wallpaperFlags + if (Build.VERSION.SDK_INT >= 34) { + this.wallpaperServiceEngine?.let { + return it.wallpaperFlags + } } return WALLPAPER_FLAG_NOT_FOUND } diff --git a/weathereffects/Android.bp b/weathereffects/Android.bp new file mode 100644 index 0000000..0382b62 --- /dev/null +++ b/weathereffects/Android.bp @@ -0,0 +1,76 @@ +// Copyright (C) 2023 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_library { + name: "WeatherEffectsLib", + manifest: "AndroidManifest.xml", + sdk_version: "system_current", + // min_sdk version must be specified to not compile against platform apis. + // Using RuntimeShader requires minimum of 33. + min_sdk_version: "33", + static_libs: [ + "androidx.slice_slice-core", + "androidx.slice_slice-builders", + "dagger2", + "jsr330", // Dagger inject annotations. + "kotlinx_coroutines_android", + "kotlinx_coroutines", + "androidx.core_core-ktx", + "androidx.appcompat_appcompat", + "androidx-constraintlayout_constraintlayout", + "toruslib", + ], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + // TODO(b/300991599): Split out debug source. + "debug/src/**/*.java", + "debug/src/**/*.kt" + ], + resource_dirs: [ + "res", + // TODO(b/300991599): Split out debug resources. + "debug/res" + ], + javacflags: ["-Adagger.fastInit=enabled"], + kotlincflags: ["-Xjvm-default=all"], + plugins: ["dagger2-compiler"], + dxflags: ["--multi-dex"], + // This library is meant to access only public APIs, do not flip this flag to true. + platform_apis: false +} + +android_app { + name: "WeatherEffects", + owner: "google", + privileged: false, + sdk_version: "system_current", + min_sdk_version: "33", + static_libs: [ + "WeatherEffectsLib" + ], + use_embedded_native_libs: true, + optimize: { + enabled: true, + shrink: true, + shrink_resources: true, + }, +} + +// TODO(b/300991599): setup test configuration. + diff --git a/weathereffects/AndroidManifest.xml b/weathereffects/AndroidManifest.xml new file mode 100644 index 0000000..bdcd956 --- /dev/null +++ b/weathereffects/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/weathereffects/assets/shaders/color_grading_lut.agsl b/weathereffects/assets/shaders/color_grading_lut.agsl new file mode 100644 index 0000000..9310369 --- /dev/null +++ b/weathereffects/assets/shaders/color_grading_lut.agsl @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 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. + */ + +uniform shader texture; +uniform shader lut; +uniform float intensity; +/* + * The LUT cube size, in pixels (depth, width and height). If we use the OpenGL coordinate + * system (right-handed, Y-up, X-right, negative Z-forward), each direction represents a color change: + * - Right changes (width) are related to red changes. + * - Up changes (height) are related to blue changes. + * - Forward changes (depth) are related to green changes. + */ +const float LUT_CUBE_SIZE = 32.0; + +/* + * Each height unit of the LUT cube is sliced into an LUT_CUBE_SIZE x LUT_CUBE_SIZE image; + * then all the images are concatenated on the texture image width. Thus, the image height + * is LUT_CUBE_SIZE and the width is LUT_CUBE_SIZE x LUT_CUBE_SIZE. Each slice will have a + * different blue value, and inside each slice, vertical direction means green color changes and + * horizontal direction means red color changes. + */ +const float IMAGE_WIDTH = LUT_CUBE_SIZE * LUT_CUBE_SIZE; + +// The last slice first pixel index. +const float LAST_SLICE_FIRST_IDX = IMAGE_WIDTH - LUT_CUBE_SIZE; + +// The last pixel index of a slice. +const float SLICE_LAST_IDX = LUT_CUBE_SIZE - 1.; +const float SLICE_LAST_IDX_INV = 1. / SLICE_LAST_IDX; + +// Applies lut, pass in texture color to apply to. +vec4 main(float2 fragCoord) { + vec4 color = texture.eval(fragCoord); + + /* + * When we fetch the new color on the LUT cube, each color can fall in between two values + * (pixels) which might not be one next to each other. we need to calculate both values and + * interpolate. + */ + vec3 colorTmp = color.rgb * SLICE_LAST_IDX; + + // Calculate the floor UVs. + vec3 colorFloor = floor(colorTmp) * SLICE_LAST_IDX_INV; + ivec2 uvFloor = ivec2( + int(colorFloor.b * LAST_SLICE_FIRST_IDX + colorFloor.r * SLICE_LAST_IDX), + int(colorFloor.g * SLICE_LAST_IDX)); + + // Calculate the ceil UVs. + vec3 colorCeil = ceil(colorTmp) * SLICE_LAST_IDX_INV; + ivec2 uvCeil = ivec2( + int(colorCeil.b * LAST_SLICE_FIRST_IDX + colorCeil.r * SLICE_LAST_IDX), + int(colorCeil.g * SLICE_LAST_IDX)); + + /* + * Fetch the color from the LUT, and combine both floor and ceiling options based on the + * fractional part. + */ + vec4 colorAfterLut = vec4(0., 0., 0., color.a); + colorAfterLut.rgb = mix( + lut.eval(vec2(uvFloor.x, uvFloor.y)).rgb, + lut.eval(vec2(uvCeil.x, uvCeil.y)).rgb, + fract(colorTmp) + ); + + return mix(color, colorAfterLut, intensity); +} diff --git a/weathereffects/assets/shaders/constants.agsl b/weathereffects/assets/shaders/constants.agsl new file mode 100644 index 0000000..5f710d8 --- /dev/null +++ b/weathereffects/assets/shaders/constants.agsl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2023 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. + */ + +/* Constants. */ +const float PI = 3.14159265359; +const float TAU = 6.2831853072; diff --git a/weathereffects/assets/shaders/fog_effect.agsl b/weathereffects/assets/shaders/fog_effect.agsl new file mode 100644 index 0000000..8c5ec46 --- /dev/null +++ b/weathereffects/assets/shaders/fog_effect.agsl @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2023 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. + */ + +uniform shader foreground; +uniform shader background; +uniform float2 uvOffsetFgd; +uniform float2 uvScaleFgd; +uniform float2 uvOffsetBgd; +uniform float2 uvScaleBgd; +uniform float2 timeForeground; +uniform float2 timeBackground; +uniform float screenAspectRatio; +uniform float2 screenSize; + +#include "shaders/constants.agsl" +#include "shaders/utils.agsl" +#include "shaders/simplex_noise.agsl" + +// TODO: explore a more performant way of generating the volumetric fog effect. +const int numOctaves = 2; + +float fbm (vec3 st, vec2 time) { + vec3 p = st; + float a = 0.5; + float result = 0.0; + + for (int i = 0; i < numOctaves; i++) { + result += a * simplex3d(vec3(p.xy + time, p.z)); + p *= 2.0152; + a *= 0.5; + } + + return result; +} + +vec3 normalBlendWithWhiteSrc(vec3 b, float o) { + return b * (1. - o) + o; +} + +vec3 normalBlend(vec3 b, vec3 f, float o) { + return b * (1. - o) + f * o; +} + +vec4 main(float2 fragCoord) { + float2 uv = fragCoord / screenSize; + uv.y /= screenAspectRatio; + + vec4 colorForeground = foreground.eval(fragCoord * uvScaleFgd + uvOffsetFgd); + vec4 color = background.eval(fragCoord * uvScaleBgd + uvOffsetBgd); + + float frontFog = smoothstep(-0.616, 0.552, fbm(vec3(uv * 0.886, 123.1), timeForeground)); + float bgdFog = smoothstep(-0.744, 0.28, fbm(vec3(uv * 1.2, 231.), timeBackground)); + + float dithering = (1. - idGenerator(uv) * 0.161); + + color.rgb = normalBlendWithWhiteSrc(color.rgb, 0.8 * dithering * bgdFog); + // Add the foreground. Any effect from here will be in front of the subject. + color.rgb = normalBlend(color.rgb, colorForeground.rgb, colorForeground.a); + // foreground fog. + color.rgb = normalBlendWithWhiteSrc(color.rgb, 0.5 * frontFog); + return color; +} diff --git a/weathereffects/assets/shaders/glass_rain.agsl b/weathereffects/assets/shaders/glass_rain.agsl new file mode 100644 index 0000000..9e0b9c5 --- /dev/null +++ b/weathereffects/assets/shaders/glass_rain.agsl @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2023 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. + */ + +struct GlassRain { + highp vec2 drop; + highp float dropMask; + highp vec2 dropplets; + highp float droppletsMask; + highp float trailMask; + highp vec2 cellUv; +}; + +/** + * Generates information to show some rain running down a foggy glass surface. + * + * @param uv the UV of the fragment where we will display the rain effect. + * @param screenAspectRatio the aspect ratio of the fragment where we will display the effect. + * @param time the elapsed time. + * @param rainGridSize the size of the grid, where each cell contains a main drop and some + * dropplets. + * @param rainIntensity how many of the cells will contain drops. Value from 0 (no rain) to 1 + * (each cell contains a drop). + * + * @returns GlassRain an object containing all the info to draw the rain. + */ +GlassRain generateGlassRain( + // UVs of the target fragment (normalized). + in vec2 uv, + in float screenAspectRatio, + in float time, + in vec2 rainGridSize, + in float rainIntensity +) { + vec2 dropPos = vec2(0.); + float cellMainDropMask = 0.0; + vec2 trailDropsPos = vec2(0.); + float cellDroppletsMask = 0.0; + float cellTrailMask = 0.0; + + /* Grid. */ + // Number of rows and columns (each one is a cell, a drop). + float cellAspectRatio = rainGridSize.x / rainGridSize.y; + // Aspect ratio impacts visible cells. + rainGridSize.y /= screenAspectRatio; + // scale the UV to allocate number of rows and columns. + vec2 gridUv = uv * rainGridSize; + // Invert y (otherwise it goes from 0=top to 1=bottom). + gridUv.y = 1. - gridUv.y; + float verticalGridPos = 2.4 * time / 5.0; + // Move grid vertically down. + gridUv.y += verticalGridPos; + + /* Cell. */ + // Get the cell ID based on the grid position. Value from 0 to 1. + float cellId = idGenerator(floor(gridUv)); + // For each cell, we set the internal UV from -0.5 (left, bottom) to 0.5 (right, top). + vec2 cellUv = fract(gridUv) - 0.5; + + /* Cell-id-based variations. */ + // Adjust time based on cellId. + time += cellId * 7.1203; + // Adjusts UV.y based on cell ID. This will make that the wiggle variation is different for + // each cell. + uv.y += cellId * 3.83027; + // Adjusts scale of each drop (higher is smaller). + float scaleVariation = 1.0 + 0.7 * cellId; + // Make some cells to not have drops. + if (cellId < 1. - rainIntensity) { + return GlassRain(dropPos, cellMainDropMask, trailDropsPos, cellDroppletsMask, + cellTrailMask, cellUv); + } + + /* Cell main drop. */ + // vertical movement: Fourier Series-Sawtooth Wave (ascending: /|/|/|). + float verticalSpeed = TAU / 5.0; + float verticalPosVariation = 0.45 * 0.63 * ( + -1.2 * sin(verticalSpeed * time) + -0.5 * sin(2. * verticalSpeed * time) + -0.3333 * sin(3. * verticalSpeed * time) + ); + + // Horizontal movement: Wiggle. + float wiggleSpeed = 6.0; + float wiggleAmp = 0.5; + // Define the start based on the cell id. + float horizontalStartAmp = 0.5; + float horizontalStart = (cellId - 0.5) * 2.0 * horizontalStartAmp / cellAspectRatio; + // Add the wiggle (equation decided by testing in Grapher). + float horizontalWiggle = wiggle(uv.y, wiggleSpeed); + + // Add the start and wiggle and make that when we are closer to the edge, we don't wiggle much + // (so the drop doesn't go outside it's cell). + horizontalWiggle = horizontalStart + + (horizontalStartAmp - abs(horizontalStart)) * wiggleAmp * horizontalWiggle; + + // Calculate main cell drop. + float dropPosUncorrected = (cellUv.x - horizontalWiggle); + dropPos.x = dropPosUncorrected / cellAspectRatio; + // Create tear drop shape. + verticalPosVariation -= dropPosUncorrected * dropPosUncorrected / cellAspectRatio; + dropPos.y = cellUv.y - verticalPosVariation; + // Adjust scale. + dropPos *= scaleVariation; + // Create a circle for the main drop in the cell, based on position. + cellMainDropMask = smoothstep(0.06, 0.04, length(dropPos)); + + /* Cell trail dropplets. */ + trailDropsPos.x = (cellUv.x - horizontalWiggle)/ cellAspectRatio; + // Substract verticalGridPos to mage the dropplets stick in place. + trailDropsPos.y = cellUv.y -verticalGridPos; + trailDropsPos.y = (fract(trailDropsPos.y * 4.) - 0.5) / 4.; + // Adjust scale. + trailDropsPos *= scaleVariation; + cellDroppletsMask = smoothstep(0.03, 0.02, length(trailDropsPos)); + // Fade the dropplets frop the top the farther they are from the main drop. + // Multiply by 1.2 so we show more of the trail. + float verticalFading = 1.2 * smoothstep(0.5, verticalPosVariation, cellUv.y); + cellDroppletsMask *= verticalFading; + // Mask dropplets that are under main cell drop. + cellDroppletsMask *= smoothstep(-0.06, 0.08, dropPos.y); + + /* Cell trail mask (it will show the image unblurred). */ + // Gradient for top of the main drop. + cellTrailMask = smoothstep(-0.04, 0.04, dropPos.y); + // Fades out the closer we get to the top of the cell. + cellTrailMask *= verticalFading; + // Only show the main section of the trail. + cellTrailMask *= smoothstep(0.07, 0.02, abs(dropPos.x)); + + cellDroppletsMask *= cellTrailMask; + + return GlassRain( + dropPos, cellMainDropMask, trailDropsPos, cellDroppletsMask, cellTrailMask, cellUv); +} diff --git a/weathereffects/assets/shaders/rain.agsl b/weathereffects/assets/shaders/rain.agsl new file mode 100644 index 0000000..eed39f8 --- /dev/null +++ b/weathereffects/assets/shaders/rain.agsl @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 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. + */ + +struct Rain { + highp float dropMask; + highp vec2 cellUv; +}; + +/** + * Pouring rain. + * + * @param uv the UV of the fragment where we will display the rain effect. + * @param screenAspectRatio the aspect ratio of the fragment where we will display the effect. + * @param time the elapsed time. + * @param rainGridSize the size of the grid, where each cell contains a main drop and some + * dropplets. + * @param rainIntensity how many of the cells will contain drops. Value from 0 (no rain) to 1 + * (each cell contains a drop). + * + * @returns float with the rain info. + */ +Rain generateRain( + // UVs of the target fragment (normalized). + in vec2 uv, + in float screenAspectRatio, + in float time, + in vec2 rainGridSize, + in float rainIntensity +) { + /* Grid. */ + // Number of rows and columns (each one is a cell, a drop). + float cellAspectRatio = rainGridSize.x / rainGridSize.y; + // Aspect ratio impacts visible cells. + rainGridSize.y /= screenAspectRatio; + // scale the UV to allocate number of rows and columns. + vec2 gridUv = uv * rainGridSize; + // Invert y (otherwise it goes from 0=top to 1=bottom). + gridUv.y = 1. - gridUv.y; + float verticalGridPos = 0.4 * time; + // Move grid vertically down. + gridUv.y += verticalGridPos; + // Generate column id, to offset columns vertically (so rain is not aligned). + float columnId = idGenerator(floor(gridUv.x)); + gridUv.y += columnId * 2.6; + + /* Cell. */ + // Get the cell ID based on the grid position. Value from 0 to 1. + float cellId = idGenerator(floor(gridUv)); + // For each cell, we set the internal UV from -0.5 (left, bottom) to 0.5 (right, top). + vec2 cellUv = fract(gridUv) - 0.5; + + float intensity = idGenerator(floor(vec2(cellId * 8.16, 27.2))); + if (intensity < 1. - rainIntensity) { + return Rain(0.0, cellUv); + } + + /* Cell-id-based variations. */ + // Adjust time based on columnId. + time += columnId * 7.1203; + // Adjusts scale of each drop (higher is smaller). + float scaleVariation = 1.0 - 0.3 * cellId; + float opacityVariation = (1. - 0.9 * cellId); + + /* Cell drop. */ + // Define the start based on the cell id. + float horizontalStart = 0.8 * (cellId - 0.5); + + // Calculate drop. + vec2 dropPos = cellUv; + dropPos.y += -0.052; + dropPos.x += horizontalStart; + dropPos *= scaleVariation * vec2(14.2, 2.728); + // Create the drop. + float dropMask = smoothstep( + 0., + // Adjust the opacity. + .80 + 3. * cellId, + // Adjust the shape. + 1. - length(vec2(dropPos.x, (dropPos.y - dropPos.x * dropPos.x))) + ); + + return Rain(dropMask, cellUv); +} diff --git a/weathereffects/assets/shaders/rain_effect.agsl b/weathereffects/assets/shaders/rain_effect.agsl new file mode 100644 index 0000000..65ee13b --- /dev/null +++ b/weathereffects/assets/shaders/rain_effect.agsl @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2023 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. + */ + +uniform shader foreground; +uniform shader background; +uniform shader blurredBackground; +uniform float2 uvOffsetFgd; +uniform float2 uvScaleFgd; +uniform float2 uvOffsetBgd; +uniform float2 uvScaleBgd; +uniform float time; +uniform float screenAspectRatio; +uniform float2 screenSize; + +#include "shaders/constants.agsl" +#include "shaders/utils.agsl" +#include "shaders/glass_rain.agsl" +#include "shaders/rain.agsl" + +/* Constants that can be modified. */ +// The color of the highlight of each drop. +const vec4 highlightColor = vec4(vec3(1.), 1.); // white +// The color of the contact ambient occlusion shadow. +const vec4 contactShadowColor = vec4(vec3(0.), 1.); // black +// The color of the contact ambient occlusion shadow. +const vec4 dropTint = vec4(vec3(1.), 1.); // white +// Glass tint. +const vec4 glassTint = vec4(vec3(0.5), 1.); // gray +// rain tint. +const vec4 rainTint = vec4(vec3(0.5), 1.); // gray + +// How much of tint we want in the drops. +const float dropTintIntensity = 0.09; +// How much of highlight we want. +const float uHighlightIntensity = 0.7; +// How much of contact shadow we want. +const float uDropShadowIntensity = 0.5; +// rain visibility (how visible it is). +const float rainVisibility = 0.4; +// How heavy it rains. 1: heavy showers; 0: no rain. +const float rainIntensity = 0.21; + +vec4 main(float2 fragCoord) { + float2 uv = fragCoord / screenSize; + // Adjusts the UVs to have the expected rect of the image. + float2 uvTextureBgd = fragCoord * uvScaleBgd + uvOffsetBgd; + vec4 colorForeground = foreground.eval(fragCoord * uvScaleFgd + uvOffsetFgd); + vec4 color = vec4(0., 0., 0., 1.); + + // Generate small glass rain. + GlassRain drippingRain = generateGlassRain( + uv, + screenAspectRatio, + time, + /* Grid size = */ vec2(4.0, 2.0), + /* rain intensity = */ 0.8); + float dropMask = drippingRain.dropMask; + float droppletsMask = drippingRain.droppletsMask; + float trailMask = drippingRain.trailMask; + vec2 dropUvMasked = drippingRain.drop * drippingRain.dropMask; + vec2 droppletsUvMasked = drippingRain.dropplets * drippingRain.droppletsMask; + + // Generate medium glass rain and combine with small one. + drippingRain = generateGlassRain( + uv, + screenAspectRatio, + time * 1.267, + /* Grid size = */ vec2(3.0, 1.0), + /* rain intensity = */ 0.6); + dropMask = max(drippingRain.dropMask, dropMask); + droppletsMask = max(drippingRain.droppletsMask, droppletsMask); + trailMask = max(drippingRain.trailMask, trailMask); + dropUvMasked = mix(dropUvMasked, + drippingRain.drop * drippingRain.dropMask, drippingRain.dropMask); + droppletsUvMasked = mix(droppletsUvMasked, + drippingRain.dropplets * drippingRain.droppletsMask, drippingRain.droppletsMask); + + /* Generate distortion UVs. */ + // UV distortion for the drop and dropplets. + float distortionDrop = 1.0; + float distortionDropplets = 0.6; + vec2 uvDiffractionOffsets = + distortionDrop * dropUvMasked + distortionDropplets * droppletsUvMasked; + + // Get color of the background texture. + color = background.eval(uvTextureBgd + uvDiffractionOffsets * screenSize); + + // Add some slight tint to the fog glass. Since we use gray, we reduce the contrast. + vec3 blurredImage = mix(blurredBackground.eval(uvTextureBgd).rgb, glassTint.rgb, 0.07); + // The blur mask (when we will show the regular background). + float blurMask = smoothstep(0.5, 1., max(trailMask, max(dropMask, droppletsMask))); + color.rgb = mix(blurredImage, color.rgb, blurMask); + + /* + * Drop coloring. This section is important to make the drops visible when we have a solid + * color as a background (since we rely normally on the UV distortion). + */ + // Tint the rain drops. + color.rgb = mix( + color.rgb, + dropTint.rgb, + dropTintIntensity * smoothstep(0.7, 1., max(dropMask, droppletsMask))); + // add highlight to drops. + color.rgb = mix( + color.rgb, + highlightColor.rgb, + uHighlightIntensity + // Adjust this scalars to make it visible. + * smoothstep(0.05, 0.08, max(dropUvMasked * 1.4, droppletsUvMasked * 1.7)).x); + // add shadows to drops. + color.rgb = mix( + color.rgb, + contactShadowColor.rgb, + uDropShadowIntensity * + // Adjust this scalars to make it visible. + smoothstep(0.055, 0.1, max(length(dropUvMasked * 1.7), + length(droppletsUvMasked * 1.9)))); + + // Add rotation for the rain (as a default sin(time * 0.05) can be used). + float variation = wiggle(time - uv.y * 1.1, 0.10); + uv = rotateAroundPoint(uv, vec2(0.5, -1.42), variation * PI / 9.); + + // Generate rain behind the subject. + Rain rain = generateRain( + uv, + screenAspectRatio, + time * 18., + /* Grid size = */ vec2(30.0, 4.0), + /* rain intensity = */ rainIntensity); + + color.rgb = mix(color.rgb, highlightColor.rgb, rainVisibility * rain.dropMask); + + // Add the foreground. Any effect from here will be in front of the subject. + color.rgb = mix(color.rgb, colorForeground.rgb, colorForeground.a); + + // Generate rain in front of the subject (bigger and faster). + rain = generateRain( + uv, + screenAspectRatio, + time * 27., + /* Grid size = */ vec2(8.0, 3.0), + /* rain intensity = */ rainIntensity); + + // The rain that is closer, make it less visible + color.rgb = mix(color.rgb, highlightColor.rgb, 0.7 * rainVisibility * rain.dropMask); + + /* Debug rain drops on glass */ + // resets color. + // color.rgb *= 0.; + // Shows the UV of each cell. + // color.rg = drippingRain.cellUv.xy; + // Shows the grid. + // if (drippingRain.cellUv.x > 0.49 || drippingRain.cellUv.y > 0.49) color.r = 1.0; + // Shows the main drop mask. + // color.rgb += drippingRain.dropMask; + // Shows the dropplets mask. + // color.rgb += drippingRain.droppletsMask; + // Shows the trail. + // color.rgb += drippingRain.trailMask * 0.3; + // background blurMask. + // color.rgb += blurMask; + // tears uv. + // color.rg += -droppletsUvMasked; + + /* Debug rain */ + // resets color. + // color.rgb *= 0.; + // color.rgb += rain.dropMask; + // if (rain.cellUv.x > 0.49 || rain.cellUv.y > 0.49) color.r = 1.0; + + return color; +} diff --git a/weathereffects/assets/shaders/simplex_noise.agsl b/weathereffects/assets/shaders/simplex_noise.agsl new file mode 100644 index 0000000..7c696f9 --- /dev/null +++ b/weathereffects/assets/shaders/simplex_noise.agsl @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 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. + */ + +// Copied from frameworks/base/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ +// shaderutil/ShaderUtilLibrary.kt + +// Return range [-1, 1]. +vec3 hash(vec3 p) { + p = fract(p * vec3(.3456, .1234, .9876)); + p += dot(p, p.yxz + 43.21); + p = (p.xxy + p.yxx) * p.zyx; + return (fract(sin(p) * 4567.1234567) - .5) * 2.; +} +// Skew factors (non-uniform). +const float SKEW = 0.3333333; // 1/3 +const float UNSKEW = 0.1666667; // 1/6 + +// Return range roughly [-1,1]. +// It's because the hash function (that returns a random gradient vector) returns +// different magnitude of vectors. Noise doesn't have to be in the precise range thus +// skipped normalize. +float simplex3d(vec3 p) { + // Skew the input coordinate, so that we get squashed cubical grid + vec3 s = floor(p + (p.x + p.y + p.z) * SKEW); + + // Unskew back + vec3 u = s - (s.x + s.y + s.z) * UNSKEW; + + // Unskewed coordinate that is relative to p, to compute the noise contribution + // based on the distance. + vec3 c0 = p - u; + + // We have six simplices (in this case tetrahedron, since we are in 3D) that we + // could possibly in. + // Here, we are finding the correct tetrahedron (simplex shape), and traverse its + // four vertices (c0..3) when computing noise contribution. + // The way we find them is by comparing c0's x,y,z values. + // For example in 2D, we can find the triangle (simplex shape in 2D) that we are in + // by comparing x and y values. i.e. x>y lower, xy0>z0: (1,0,0), (1,1,0), (1,1,1) + // x0>z0>y0: (1,0,0), (1,0,1), (1,1,1) + // z0>x0>y0: (0,0,1), (1,0,1), (1,1,1) + // z0>y0>x0: (0,0,1), (0,1,1), (1,1,1) + // y0>z0>x0: (0,1,0), (0,1,1), (1,1,1) + // y0>x0>z0: (0,1,0), (1,1,0), (1,1,1) + // + // The rule is: + // * For offset1, set 1 at the max component, otherwise 0. + // * For offset2, set 0 at the min component, otherwise 1. + // * For offset3, set 1 for all. + // + // Encode x0-y0, y0-z0, z0-x0 in a vec3 + vec3 en = c0 - c0.yzx; + + // Each represents whether x0>y0, y0>z0, z0>x0 + en = step(vec3(0.), en); + + // en.zxy encodes z0>x0, x0>y0, y0>x0 + vec3 offset1 = en * (1. - en.zxy); // find max + vec3 offset2 = 1. - en.zxy * (1. - en); // 1-(find min) + vec3 offset3 = vec3(1.); + + vec3 c1 = c0 - offset1 + UNSKEW; + vec3 c2 = c0 - offset2 + UNSKEW * 2.; + vec3 c3 = c0 - offset3 + UNSKEW * 3.; + + // Kernel summation: dot(max(0, r^2-d^2))^4, noise contribution) + // + // First compute d^2, squared distance to the point. + vec4 w; // w = max(0, r^2 - d^2)) + w.x = dot(c0, c0); + w.y = dot(c1, c1); + w.z = dot(c2, c2); + w.w = dot(c3, c3); + + // Noise contribution should decay to zero before they cross the simplex boundary. + // Usually r^2 is 0.5 or 0.6; + // 0.5 ensures continuity but 0.6 increases the visual quality for the application + // where discontinuity isn't noticeable. + w = max(0.6 - w, 0.); + + // Noise contribution from each point. + vec4 nc; + nc.x = dot(hash(s), c0); + nc.y = dot(hash(s + offset1), c1); + nc.z = dot(hash(s + offset2), c2); + nc.w = dot(hash(s + offset3), c3); + + nc *= w * w * w * w; + + // Add all the noise contributions. + // Should multiply by the possible max contribution to adjust the range in [-1,1]. + return dot(vec4(32.), nc); +} diff --git a/weathereffects/assets/shaders/snow.agsl b/weathereffects/assets/shaders/snow.agsl new file mode 100644 index 0000000..9d63a0d --- /dev/null +++ b/weathereffects/assets/shaders/snow.agsl @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2023 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. + */ + +struct Snow { + highp float flakeMask; + highp vec2 cellUv; +}; + +const mat2 rot45 = mat2( + 0.7071067812, 0.7071067812, // First column. + -0.7071067812, 0.7071067812 // second column. +); + +/** + * Generates snow flakes. + * + * @param uv the UV of the fragment where we will display the snow effect. + * @param screenAspectRatio the aspect ratio of the fragment where we will display the effect. + * @param time the elapsed time. + * @param snowGridSize the size of the grid, where each cell contains a snow flake. + * @param layerNumber the layer of snow that we want to draw. Higher is farther from camera. + * + * @returns Snow with the snow info. + */ +Snow generateSnow( + // UVs of the target fragment (normalized). + in vec2 uv, + in float screenAspectRatio, + in float time, + in vec2 snowGridSize, + in float layerNumber +) { + /* Grid. */ + float depth = 1. + layerNumber * 0.3; + float speedAdj = 1. + layerNumber * 0.15; + float layerR = idGenerator(layerNumber); + snowGridSize *= depth; + time += layerR * 58.3; + // Number of rows and columns (each one is a cell, a drop). + float cellAspectRatio = snowGridSize.x / snowGridSize.y; + // Aspect ratio impacts visible cells. + snowGridSize.y /= screenAspectRatio; + // Skew uv.x so it goes to left or right + uv.x += uv.y * (0.8 * layerR - 0.4); + // scale the UV to allocate number of rows and columns. + vec2 gridUv = uv * snowGridSize; + // Invert y (otherwise it goes from 0=top to 1=bottom). + gridUv.y = 1. - gridUv.y; + float verticalGridPos = 0.4 * time / speedAdj; + // Move grid vertically down. + gridUv.y += verticalGridPos; + // Generate column id, to offset columns vertically (so snow flakes are not aligned). + float columnId = idGenerator(floor(gridUv.x)); + gridUv.y += columnId * 2.6; + + /* Cell. */ + // Get the cell ID based on the grid position. Value from 0 to 1. + float cellId = idGenerator(floor(gridUv)); + // For each cell, we set the internal UV from -0.5 (left, bottom) to 0.5 (right, top). + vec2 cellUv = fract(gridUv) - 0.5; + cellUv.y *= -1.; + + /* Cell-id-based variations. */ + // Adjust time based on columnId. + // Adjusts scale of each snow flake (higher is smaller). + float scaleVariation = 2.0 + 2.7 * cellId; + float opacityVariation = (1. - 0.9 * cellId); + + /* Cell snow flake. */ + + // Horizontal movement: Wiggle. + float wiggleSpeed = 3.0; + float wiggleAmp = 0.8; + // Define the start based on the cell id. + float horizontalStartAmp = 0.5; + // Add the wiggle (equation decided by testing in Grapher). + float horizontalWiggle = wiggle(uv.y + cellId * 2.1, wiggleSpeed * speedAdj); + + // Add the start and wiggle and make that when we are closer to the edge, we don't wiggle much + // (so the drop doesn't go outside it's cell). + horizontalWiggle = horizontalStartAmp * wiggleAmp * horizontalWiggle; + + // Calculate main cell drop. + float snowFlakePosUncorrected = (cellUv.x - horizontalWiggle); + + // Calculate snow flake. + vec2 snowFlakeShape = vec2(1., 1.2); + vec2 snowFlakePos = vec2(snowFlakePosUncorrected / cellAspectRatio, cellUv.y); + snowFlakePos -= vec2(0., uv.y - 0.5) * cellId; + snowFlakePos *= snowFlakeShape * scaleVariation; + vec2 snowFlakePosR = 1.016 * abs(rot45 * (snowFlakePos + (cellId * 2. - 1.) * vec2(0.050))); + snowFlakePos = abs(snowFlakePos); + // Create the snowFlake mask. + float flakeMask = smoothstep( + 0.3, + 0.200 - 0.3 * opacityVariation, + snowFlakePos.x + snowFlakePos.y + snowFlakePosR.x + snowFlakePosR.y + ) * opacityVariation; + + return Snow(flakeMask, cellUv); +} diff --git a/weathereffects/assets/shaders/snow_effect.agsl b/weathereffects/assets/shaders/snow_effect.agsl new file mode 100644 index 0000000..e78001e --- /dev/null +++ b/weathereffects/assets/shaders/snow_effect.agsl @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 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. + */ + +uniform shader foreground; +uniform shader background; +uniform shader blurredBackground; +uniform float2 uvOffsets; +uniform float uvScale; +uniform float time; +uniform float screenAspectRatio; +uniform float2 screenSize; + +#include "shaders/constants.agsl" +#include "shaders/utils.agsl" +#include "shaders/snow.agsl" + +/* Constants that can be modified. */ +// Snow tint. +const vec4 snowColor = vec4(vec3(0.9), 1.); +// Glass tint. +const vec4 glassTint = vec4(vec3(0.8), 1.); // gray + +// snow opacity (how visible it is). +const float snowOpacity = 0.8; + +// how frosted the glass is. +const float frostedGlassIntensity = 0.07; + +vec4 main(float2 fragCoord) { + float2 uv = fragCoord / screenSize; + // Adjusts the UVs to have the expected rect of the image. + float2 uvTexture = fragCoord * uvScale + uvOffsets; + + vec4 colorForeground = foreground.eval(uvTexture); + vec4 color = vec4(0., 0., 0., 1.); + + // Add some slight tint to the frosted glass. + + // Get color of the background texture. + color.rgb = mix(background.eval(uvTexture).rgb, glassTint.rgb, frostedGlassIntensity); + for (float i = 9.; i > 2.; i--) { + // Generate snow behind the subject. + Snow snow = generateSnow( + /* uvNorm = */ uv, + /* fragment aspect ratio = */ screenAspectRatio, + /* time = */ time * 1.25, + /* Grid size = */ vec2(3.0, 2.0), + /* layer number = */ i); + + color.rgb = mix(color.rgb, snowColor.rgb, snowOpacity * snow.flakeMask); + } + + // Add the foreground. Any effect from here will be in front of the subject. + color.rgb = mix(color.rgb, colorForeground.rgb, colorForeground.a); + + for (float i = 2.; i >= 0.; i--) { + // Generate snow behind the subject. + Snow snow = generateSnow( + /* uvNorm = */ uv, + /* fragment aspect ratio = */ screenAspectRatio, + /* time = */ time * 1.25, + /* Grid size = */ vec2(3.0, 2.0), + /* layer number = */ i); + + color.rgb = mix(color.rgb, snowColor.rgb, snowOpacity * snow.flakeMask); + } + + /* Debug snow */ + // resets color. + // color.rgb *= 0.; + // color.rgb += snow.flakeMask; + // if (snow.cellUv.x > 0.49 || snow.cellUv.y > 0.49) color.r = 1.0; + + return color; +} diff --git a/weathereffects/assets/shaders/utils.agsl b/weathereffects/assets/shaders/utils.agsl new file mode 100644 index 0000000..6c26570 --- /dev/null +++ b/weathereffects/assets/shaders/utils.agsl @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 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. + */ + +highp float idGenerator(vec2 point) { + vec2 p = fract(point * vec2(723.123, 236.209)); + p += dot(p, p + 17.1512); + return fract(p.x * p.y); +} + +highp float idGenerator(float value) { + return idGenerator(vec2(value, 1.412)); +} + +mat2 rotationMat(float angleRad) { + float c=cos(angleRad); + float s=sin(angleRad); + // | c -s | + // | s c | + return mat2( + c, s, // First column. + -s, c // second column. + ); +} + +vec2 rotateAroundPoint(vec2 point, vec2 centerPoint, float angleRad) { + return (point - centerPoint) * rotationMat(angleRad) + centerPoint; +} + +// function created on Grapher (equation decided by testing in Grapher). +float wiggle(float time, float wiggleSpeed) { + return sin(wiggleSpeed * time + 0.5 * sin(wiggleSpeed * 5. * time)) + * sin(wiggleSpeed * time) - 0.5; +} diff --git a/weathereffects/assets/textures/lut_rain_and_fog.png b/weathereffects/assets/textures/lut_rain_and_fog.png new file mode 100644 index 0000000..7e8af9c Binary files /dev/null and b/weathereffects/assets/textures/lut_rain_and_fog.png differ diff --git a/weathereffects/build.gradle b/weathereffects/build.gradle new file mode 100644 index 0000000..d51146d --- /dev/null +++ b/weathereffects/build.gradle @@ -0,0 +1,161 @@ +// Copyright (C) 2023 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. +buildscript { + ext.versions = [ + 'gradle' : '7.4.2', + 'minSdk' : 33, + 'targetSdk' : 33, + 'compileSdk' : 34, + 'buildTools' : '30.0.3', + 'kotlin' : '1.6.21', + 'ktx' : '1.10.1', + 'coroutines' : '1.6.4', + 'appcompat' : '1.6.1', + 'androidXLib' : '1.1.0-alpha02', + 'androidXRun' : '1.1.0-alpha4', + 'guava' : '31.0.1-android', + 'filament' : '1.12.5', + 'dagger' : '2.44', + 'material' : '1.9.0', + 'junit' : '4.13.2', + 'androidXTest' : '1.5.0', + 'mockito' : '2.28.3', + ] + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:$versions.gradle" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'org.jetbrains.kotlin.kapt' + +android { + compileSdk versions.compileSdk + + defaultConfig { + applicationId 'com.google.android.wallpaper.weathereffects' + minSdk versions.minSdk + targetSdk versions.targetSdk + versionCode 1 + versionName '0.1.0' + signingConfig signingConfigs.debug + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + sourceSets { + main { + // TODO: Split out debug source. + java.srcDirs = ["${rootDir}/src", "${rootDir}/debug/src"] + res.srcDirs = ["${rootDir}/res", "${rootDir}/debug/res"] + assets.srcDirs = ["${rootDir}/assets"] + manifest.srcFile "AndroidManifest.xml" + } + + debug { + java.srcDirs = ["${rootDir}/debug/src"] + res.srcDirs = ["${rootDir}/debug/res"] + assets.srcDirs = ["${rootDir}/debug/assets"] + manifest.srcFile "debug/AndroidManifest.xml" + } + + test { + java.srcDirs = ["${rootDir}/unitTests/src"] + res.srcDirs = ["${rootDir}/unitTests/res"] + } + + androidTest { + java.srcDirs = ["${rootDir}/tests/src"] + res.srcDirs = ["${rootDir}/tests/res"] + } + } + + buildTypes { + debug { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + testCoverageEnabled true + } + + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + testCoverageEnabled true + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +dependencies { + implementation project(':toruslib') + + implementation "androidx.slice:slice-builders:$versions.androidXLib" + implementation "androidx.slice:slice-core:$versions.androidXLib" + implementation "androidx.core:core-ktx:$versions.ktx" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.coroutines" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines" + implementation "androidx.appcompat:appcompat:$versions.appcompat" + implementation "androidx.constraintlayout:constraintlayout:2.1.4" + + debugImplementation "com.google.android.material:material:$versions.material" + + androidTestImplementation "junit:junit:$versions.junit" + androidTestImplementation "androidx.test:core:$versions.androidXTest" + androidTestImplementation "androidx.test:rules:$versions.androidXTest" + 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.3.1" + androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito-inline:$versions.mockito" + androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito-inline-extended:$versions.mockito" + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$versions.coroutines" + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$versions.coroutines" + + // Dagger + api "com.google.dagger:dagger:$versions.dagger" + api "com.google.dagger:dagger-android:$versions.dagger" + kapt "com.google.dagger:dagger-compiler:$versions.dagger" + kapt "com.google.dagger:dagger-android-processor:$versions.dagger" + kaptAndroidTest "com.google.dagger:dagger-compiler:$versions.dagger" + kaptAndroidTest "com.google.dagger:dagger-android-processor:$versions.dagger" +} diff --git a/weathereffects/debug/AndroidManifest.xml b/weathereffects/debug/AndroidManifest.xml new file mode 100644 index 0000000..91b19cd --- /dev/null +++ b/weathereffects/debug/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/weathereffects/debug/assets/test-background.png b/weathereffects/debug/assets/test-background.png new file mode 100644 index 0000000..7d72050 Binary files /dev/null and b/weathereffects/debug/assets/test-background.png differ diff --git a/weathereffects/debug/assets/test-foreground.png b/weathereffects/debug/assets/test-foreground.png new file mode 100644 index 0000000..1a774f5 Binary files /dev/null and b/weathereffects/debug/assets/test-foreground.png differ diff --git a/weathereffects/debug/res/drawable/ic_baseline_check_24.xml b/weathereffects/debug/res/drawable/ic_baseline_check_24.xml new file mode 100644 index 0000000..c885f06 --- /dev/null +++ b/weathereffects/debug/res/drawable/ic_baseline_check_24.xml @@ -0,0 +1,25 @@ + + + + diff --git a/weathereffects/debug/res/drawable/ic_baseline_image_search_24.xml b/weathereffects/debug/res/drawable/ic_baseline_image_search_24.xml new file mode 100644 index 0000000..fe3f420 --- /dev/null +++ b/weathereffects/debug/res/drawable/ic_baseline_image_search_24.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/weathereffects/debug/res/layout/debug_activity.xml b/weathereffects/debug/res/layout/debug_activity.xml new file mode 100644 index 0000000..a05fd5e --- /dev/null +++ b/weathereffects/debug/res/layout/debug_activity.xml @@ -0,0 +1,109 @@ + + + + + + + + + + +