1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.android.wallpaper.weathereffects.graphics.utils
18 
19 import android.content.Context
20 import android.content.res.AssetManager
21 import android.graphics.Bitmap
22 import android.graphics.BitmapFactory
23 import android.graphics.Rect
24 import android.graphics.RuntimeShader
25 import android.renderscript.Allocation
26 import android.renderscript.Element
27 import android.renderscript.RenderScript
28 import android.renderscript.ScriptIntrinsicBlur
29 import android.util.SizeF
30 import androidx.annotation.FloatRange
31 
32 /** Contains functions for rendering. */
33 object GraphicsUtils {
34     /* Default width dp is calculated as default_display_width / default_display_density. */
35     private const val DEFAULT_WIDTH_DP = 1080 / 2.625f
36 
37     /**
38      * Loads a shader from an asset file.
39      *
40      * @param assetManager an [AssetManager] instance.
41      * @param path path to the shader to load.
42      * @return returns a [RuntimeShader] object.
43      */
44     fun loadShader(assetManager: AssetManager, path: String): RuntimeShader {
45         val shader = loadRawShader(assetManager, path)
46         val finalShader = resolveShaderIncludes(assetManager, shader)
47         return RuntimeShader(finalShader)
48     }
49 
50     /**
51      * Loads a Bitmap from an asset file.
52      *
53      * @param assetManager an [AssetManager] instance.
54      * @param path path to the texture bitmap to load.
55      * @return returns a Bitmap.
56      */
57     fun loadTexture(assetManager: AssetManager, path: String): Bitmap? {
58         return assetManager.open(path).use {
59             BitmapFactory.decodeStream(
60                 it,
61                 Rect(),
62                 BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.HARDWARE },
63             )
64         }
65     }
66 
67     /**
68      * Blurs an image and returns it as a new one.
69      *
70      * @param context the application.
71      * @param sourceBitmap the original image that we want to blur.
72      * @param blurRadius the amount that we want to blur (only values from 0 to 25).
73      * @param config the bitmap config (optional).
74      * @return returns a Bitmap.
75      */
76     fun blurImage(
77         context: Context,
78         sourceBitmap: Bitmap,
79         @FloatRange(from = 0.0, to = 25.0) blurRadius: Float,
80         config: Bitmap.Config = Bitmap.Config.ARGB_8888,
81     ): Bitmap {
82         // TODO: This might not be the ideal option, find a better one.
83         val blurredImage = Bitmap.createBitmap(sourceBitmap.copy(config, true))
84         val renderScript = RenderScript.create(context)
85         val blur = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript))
86         val allocationIn = Allocation.createFromBitmap(renderScript, sourceBitmap)
87         val allocationOut = Allocation.createFromBitmap(renderScript, blurredImage)
88         blur.setRadius(blurRadius)
89         blur.setInput(allocationIn)
90         blur.forEach(allocationOut)
91         allocationOut.copyTo(blurredImage)
92         return blurredImage
93     }
94 
95     /**
96      * @return the [Float] representing the aspect ratio of width / height, -1 if either width or
97      *   height is equal to or less than 0.
98      */
99     fun getAspectRatio(size: SizeF): Float {
100         val width = size.width
101         val height = size.height
102 
103         return if (width <= 0 || height <= 0) {
104             -1f
105         } else width / height
106     }
107 
108     /**
109      * Compute the weather effect default grid size. This takes into consideration the different
110      * display densities and aspect ratio so the effect looks good on displays with different sizes.
111      *
112      * @param surfaceSize the size of the surface where the wallpaper is being rendered.
113      * @param density the current display density.
114      * @return a [Float] representing the default size.
115      */
116     fun computeDefaultGridSize(surfaceSize: SizeF, density: Float): Float {
117         val displayWidthDp = surfaceSize.width / density
118         val adjustedScale =
119             when {
120                 // "COMPACT"
121                 displayWidthDp < 600 -> 1f
122                 // "MEDIUM"
123                 displayWidthDp >= 600 && displayWidthDp < 840 -> 0.9f
124                 // "EXPANDED"
125                 else -> 0.8f
126             }
127         return adjustedScale * displayWidthDp / DEFAULT_WIDTH_DP
128     }
129 
130     private fun resolveShaderIncludes(assetManager: AssetManager, string: String): String {
131         val match = Regex("[ \\t]*#include +\"([\\w\\d./]+)\"")
132         return string.replace(match) { m ->
133             val (includePath) = m.destructured
134             getResolvedShaderPath(assetManager, includePath)
135         }
136     }
137 
138     private fun getResolvedShaderPath(assetManager: AssetManager, includePath: String): String {
139         val string = loadRawShader(assetManager, includePath)
140         return resolveShaderIncludes(assetManager, string)
141     }
142 
143     private fun loadRawShader(assetManager: AssetManager, path: String): String {
144         return assetManager.open(path).bufferedReader().use { it.readText() }
145     }
146 }
147