1 /*
2  * 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.data.repository
18 
19 import android.content.Context
20 import android.graphics.Bitmap
21 import android.graphics.BitmapFactory
22 import android.util.Log
23 import com.google.android.wallpaper.weathereffects.provider.WallpaperInfoContract
24 import kotlinx.coroutines.CoroutineDispatcher
25 import kotlinx.coroutines.Dispatchers
26 import kotlinx.coroutines.withContext
27 
28 object WallpaperFileUtils {
29     /**
30      * Exports the [bitmap] to an image file in local storage.
31      * This method may take several seconds to complete, so it should be called from
32      * a background [dispatcher].
33      *
34      * @param context the [Context] of the caller
35      * @param bitmap the source to be exported
36      * @param dispatcher the dispatcher to run within.
37      * @return `true` when exported successfully
38      */
exportBitmapnull39     suspend fun exportBitmap(
40         context: Context,
41         fileName: String,
42         bitmap: Bitmap,
43         dispatcher: CoroutineDispatcher = Dispatchers.IO,
44     ): Boolean {
45         val protectedContext = asProtectedContext(context)
46         return try {
47             withContext(dispatcher) {
48                 var success: Boolean
49                 protectedContext
50                     .openFileOutput(fileName, Context.MODE_PRIVATE)
51                     .use {
52                         success = bitmap.compress(
53                             Bitmap.CompressFormat.PNG,
54                             /* quality = */ 100,
55                             it,
56                         )
57                         if (!success) {
58                             Log.e(TAG, "Failed to write the bitmap to local storage")
59                         } else {
60                             Log.i(TAG, "Wrote bitmap to local storage. filename: $fileName")
61                         }
62                     }
63                 success
64             }
65         } catch (e: Exception) {
66             Log.e(TAG, "Failed to export", e)
67             false
68         }
69     }
70 
71     /**
72      * Imports the bitmap from an absolute path. This method may take several seconds to complete,
73      * so it should be called from a background [dispatcher].
74      *
75      * @param absolutePath the absolute file path of the bitmap to be imported.
76      * @param dispatcher the dispatcher to run within.
77      * @return the imported wallpaper bitmap, or `null` if importing failed.
78      */
importBitmapFromAbsolutePathnull79     suspend fun importBitmapFromAbsolutePath(
80         absolutePath: String,
81         dispatcher: CoroutineDispatcher = Dispatchers.IO,
82     ): Bitmap? {
83         return try {
84             withContext(dispatcher) {
85                 val bitmap = BitmapFactory.decodeFile(absolutePath)
86                 if (bitmap == null) {
87                     Log.e(TAG, "Failed to decode the bitmap")
88                 }
89                 bitmap
90             }
91         } catch (e: Exception) {
92             Log.e(TAG, "Failed to import the image", e)
93             null
94         }
95     }
96 
97     /**
98      * Imports the bitmap from local storage. This method may take several seconds to complete,
99      * so it should be called from a background [dispatcher].
100      *
101      * @param fileName name of the bitmap file in local storage.
102      * @param dispatcher the dispatcher to run within.
103      * @return the imported wallpaper bitmap, or `null` if importing failed.
104      */
importBitmapFromLocalStoragenull105     suspend fun importBitmapFromLocalStorage(
106         fileName: String,
107         context: Context,
108         dispatcher: CoroutineDispatcher = Dispatchers.IO,
109     ): Bitmap? {
110         return try {
111             withContext(dispatcher) {
112                 val protectedContext = asProtectedContext(context)
113                 val inputStream = protectedContext.openFileInput(fileName)
114                 val bitmap = BitmapFactory.decodeStream(inputStream)
115                 if (bitmap == null) {
116                     Log.e(TAG, "Failed to decode the bitmap")
117                 }
118                 bitmap
119             }
120         } catch (e: Exception) {
121             Log.e(TAG, "Failed to import the image", e)
122             null
123         }
124     }
125 
126     /**
127      * Exports the last known weather, and saves it into a shared preferences file. This is
128      * needed so when we reboot the device, we have information about the last weather and we can
129      * show it (also so we don't have to wait for the weather API to fetch the current weather).
130      *
131      * @param weatherEffect the last known weather effect.
132      * @param context the [Context] of the caller.
133      */
exportLastKnownWeathernull134     fun exportLastKnownWeather(
135         weatherEffect: WallpaperInfoContract.WeatherEffect,
136         context: Context
137     ) {
138         asProtectedContext(context).getSharedPreferences(PREF_FILENAME, Context.MODE_PRIVATE)
139             .edit()
140             .putString(LAST_KNOWN_WEATHER_KEY, weatherEffect.value)
141             .apply()
142     }
143 
144     /**
145      * Imports the last known weather from shared preferences.
146      *
147      * @param context the [Context] of the caller
148      *
149      * @return the last known weather effect, or null if not found
150      */
importLastKnownWeathernull151     fun importLastKnownWeather(context: Context): WallpaperInfoContract.WeatherEffect? {
152         return WallpaperInfoContract.WeatherEffect.fromStringValue(
153             asProtectedContext(context).getSharedPreferences(
154                 PREF_FILENAME,
155                 Context.MODE_PRIVATE
156             ).getString(LAST_KNOWN_WEATHER_KEY, null)
157         )
158     }
159 
160     /**
161      * Checks if we have Foreground and Background Bitmap in local storage.
162      *
163      * @param context the [Context] of the caller
164      *
165      * @return whether both Bitmaps exists
166      */
hasBitmapsInLocalStoragenull167     fun hasBitmapsInLocalStorage(context: Context): Boolean {
168         val protectedContext = if (context.isDeviceProtectedStorage) {
169             context
170         } else {
171             context.createDeviceProtectedStorageContext()
172         }
173         val fileBgd = protectedContext.getFileStreamPath(BG_FILE_NAME)
174         val fileFgd = protectedContext.getFileStreamPath(FG_FILE_NAME)
175 
176         return fileBgd.exists() && fileFgd.exists()
177     }
178 
asProtectedContextnull179     private fun asProtectedContext(context: Context): Context {
180         return if (context.isDeviceProtectedStorage) {
181             context
182         } else {
183             context.createDeviceProtectedStorageContext()
184         }
185     }
186 
187     private const val TAG = "WallpaperFileUtils"
188     const val FG_FILE_NAME = "fg_image"
189     const val BG_FILE_NAME = "bg_image"
190     private const val PREF_FILENAME = "weather_preferences"
191     private const val LAST_KNOWN_WEATHER_KEY = "last_known_weather"
192 }
193