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