blob: 2fc9cfddb0fbc83880ebfcab0d38f3347ca9e894 [file] [log] [blame]
/*
* 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.android.wallpaper.util
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Rect
import android.location.Location
import android.location.LocationManager
import android.util.Log
import com.android.wallpaper.asset.Asset
import com.android.wallpaper.asset.BitmapUtils
import com.android.wallpaper.compat.WallpaperManagerCompat
import com.android.wallpaper.model.AdaptiveType
import com.android.wallpaper.model.AdaptiveWallpaperInfo
import com.android.wallpaper.module.BitmapCropper
import com.android.wallpaper.module.Injector
import com.android.wallpaper.module.InjectorProvider
import com.android.wallpaper.module.WallpaperPersister.DEST_BOTH
import com.android.wallpaper.module.WallpaperPersister.DEST_HOME_SCREEN
import com.android.wallpaper.module.WallpaperPreferences
import com.ibm.icu.impl.CalendarAstronomer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.io.FileNotFoundException
import java.io.IOException
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* Util class for adaptive wallpaper.
*/
object AdaptiveWallpaperUtils {
private const val TAG = "AdaptiveWallpaperUtils"
/**
* Gets adaptive wallpaper image filename.
*
* @param type the adaptive type to get filename. It should be light or dark type.
* @return the correspond adaptive type adaptive image filename.
*/
@JvmStatic
fun getAdaptiveWallpaperFilename(type: AdaptiveType): String {
return "wp-adaptive-" + type.toString().lowercase(Locale.ROOT) + ".png"
}
/**
* Saves adaptive wallpaper images.
*/
@JvmStatic
fun saveAdaptiveWallpaperImage(
context: Context, lightBitmap: Bitmap,
darkBitmap: Bitmap, myCallback: (success: Boolean, throwable: Throwable?) -> Unit
) {
try {
lightBitmap.compress(
Bitmap.CompressFormat.PNG, 100,
context.openFileOutput(
getAdaptiveWallpaperFilename(AdaptiveType.LIGHT),
Context.MODE_PRIVATE
)
)
darkBitmap.compress(
Bitmap.CompressFormat.PNG, 100,
context.openFileOutput(
getAdaptiveWallpaperFilename(AdaptiveType.DARK),
Context.MODE_PRIVATE
)
)
lightBitmap.recycle()
darkBitmap.recycle()
myCallback.invoke(/* success= */ true, null)
} catch (e: IOException) {
Log.e(TAG, "Failed to save adaptive wallpaper image.", e)
myCallback.invoke(/* success= */ false, e)
}
}
/**
* Crops and saves adaptive wallpaper images.
*/
@JvmStatic
fun cropAndSaveAdaptiveWallpaper(
context: Context,
wallpaperScale: Float,
scaledCropRect: Rect,
adaptiveWallpaperInfo: AdaptiveWallpaperInfo,
myCallback: (success: Boolean, throwable: Throwable?) -> Unit
) {
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val lightCroppedBitmap = cropAndScaleBitmap(
context,
wallpaperScale,
scaledCropRect,
adaptiveWallpaperInfo.getAdaptiveAsset(context, AdaptiveType.LIGHT)
)
val darkCroppedBitmap = cropAndScaleBitmap(
context,
wallpaperScale,
scaledCropRect,
adaptiveWallpaperInfo.getAdaptiveAsset(context, AdaptiveType.DARK)
)
if (lightCroppedBitmap == null || darkCroppedBitmap == null) {
myCallback.invoke(/* success= */ false, NullPointerException())
} else {
saveAdaptiveWallpaperImage(
context,
lightCroppedBitmap,
darkCroppedBitmap,
myCallback
)
}
}
}
private suspend fun cropAndScaleBitmap(
context: Context,
wallpaperScale: Float,
scaledCropRect: Rect, asset: Asset?
): Bitmap? = suspendCoroutine { continuation ->
InjectorProvider.getInjector().bitmapCropper.cropAndScaleBitmap(
asset,
wallpaperScale,
scaledCropRect,
WallpaperCropUtils.isRtl(context),
object : BitmapCropper.Callback {
override fun onBitmapCropped(croppedBitmap: Bitmap?) {
continuation.resume(croppedBitmap)
}
override fun onError(e: Throwable?) {
continuation.resume(null)
}
}
)
}
/**
* Gets the adaptive type calculated by CalendarAstronomer with location, if location is null
* then the switch time will be 6 a.m. and 6 p.m..
*/
@JvmStatic
fun getCurrentAdaptiveType(currentTimeMillis: Long, location: Location?): AdaptiveType {
val sunriseTimeMillis: Long
val sunsetTimeMillis: Long
val calendar = Calendar.getInstance()
if (location == null) {
calendar.timeInMillis = currentTimeMillis
calendar[Calendar.MINUTE] = 0
calendar[Calendar.SECOND] = 0
calendar[Calendar.MILLISECOND] = 0
calendar[Calendar.HOUR_OF_DAY] = 6
sunriseTimeMillis = calendar.timeInMillis
calendar[Calendar.HOUR_OF_DAY] = 18
sunsetTimeMillis = calendar.timeInMillis
} else {
val calendarAstronomer = CalendarAstronomer(
location.longitude,
location.latitude
)
calendarAstronomer.time = currentTimeMillis
sunriseTimeMillis = calendarAstronomer.getSunRiseSet(/* rise= */ true)
sunsetTimeMillis = calendarAstronomer.getSunRiseSet(/* rise= */ false)
}
return if (sunsetTimeMillis > sunriseTimeMillis) {
if (sunsetTimeMillis < currentTimeMillis || sunriseTimeMillis > currentTimeMillis) {
AdaptiveType.DARK
} else {
AdaptiveType.LIGHT
}
} else {
if (sunsetTimeMillis < currentTimeMillis && currentTimeMillis < sunriseTimeMillis) {
AdaptiveType.DARK
} else {
AdaptiveType.LIGHT
}
}
}
/**
* Rotates the adaptive wallpaper. It rotates by destination gotten from WallpaperPreferences.
* Also saves the bitmap hash code to make category check mark correct.
*
* @return false if rotates failed.
*/
fun rotateNextAdaptiveWallpaper(context: Context, nextAdaptiveType: AdaptiveType): Boolean {
val bitmap = getAdaptiveWallpaperImage(context, nextAdaptiveType) ?: return false
val bitmapHash = BitmapUtils.generateHashCode(bitmap)
val injector: Injector = InjectorProvider.getInjector()
val wallpaperStatusChecker = injector.wallpaperStatusChecker
val isLockWallpaperSet = wallpaperStatusChecker.isLockWallpaperSet(context)
val destination = if (isLockWallpaperSet) DEST_HOME_SCREEN else DEST_BOTH
val wallpaperPreferences: WallpaperPreferences = injector.getPreferences(context)
val whichWallpaper: Int
// Saves bitmap hash to make sure the check mark in individual fragment position is correct.
when (destination) {
DEST_HOME_SCREEN -> {
whichWallpaper = WallpaperManagerCompat.FLAG_SYSTEM
wallpaperPreferences.homeWallpaperHashCode = bitmapHash
}
else -> { // DEST_BOTH
whichWallpaper = (WallpaperManagerCompat.FLAG_SYSTEM
or WallpaperManagerCompat.FLAG_LOCK)
wallpaperPreferences.homeWallpaperHashCode = bitmapHash
wallpaperPreferences.lockWallpaperHashCode = bitmapHash
}
}
wallpaperPreferences.appliedAdaptiveType = nextAdaptiveType
return InjectorProvider.getInjector().getWallpaperPersister(context)
.setBitmapToWallpaperManagerCompat(bitmap, false, whichWallpaper) != 0
}
/**
* Gets the next rotate adaptive wallpaper time by AdaptiveType calculated by
* CalendarAstronomer with location.
*/
@JvmStatic
fun getNextRotateAdaptiveWallpaperTimeByAdaptiveType(
location: Location?,
nextAdaptiveType: AdaptiveType
): Long {
val timeMillis = System.currentTimeMillis()
val calendar = Calendar.getInstance()
calendar.timeInMillis = timeMillis
calendar[Calendar.MINUTE] = 0
calendar[Calendar.SECOND] = 0
calendar[Calendar.MILLISECOND] = 0
return if (location == null) {
if (nextAdaptiveType === AdaptiveType.LIGHT) {
calendar[Calendar.HOUR_OF_DAY] = 6
} else {
calendar[Calendar.HOUR_OF_DAY] = 18
}
if (timeMillis > calendar.timeInMillis) {
calendar[Calendar.DATE] = calendar[Calendar.DATE] + 1
}
calendar.timeInMillis
} else {
val calendarAstronomer = CalendarAstronomer(
location.longitude,
location.latitude
)
calendarAstronomer.time = timeMillis
var nextRotateTime = getTimeMillisByAdaptiveType(calendarAstronomer, nextAdaptiveType)
if (timeMillis > nextRotateTime) {
calendarAstronomer.time =
timeMillis + TimeUnit.DAYS.toMillis(/* duration= */ 1)
nextRotateTime = getTimeMillisByAdaptiveType(calendarAstronomer, nextAdaptiveType)
}
nextRotateTime
}
}
private fun getTimeMillisByAdaptiveType(
calendarAstronomer: CalendarAstronomer,
adaptiveType: AdaptiveType
) = when (adaptiveType) {
AdaptiveType.LIGHT -> calendarAstronomer.getSunRiseSet(/* rise= */ true)
AdaptiveType.DARK -> calendarAstronomer.getSunRiseSet(/* rise= */ false)
else -> calendarAstronomer.time
}
/**
* Gets the device location.
*/
@JvmStatic
fun getLocation(context: Context): Location? {
return if (PermissionUtils.isAccessCoarseLocationPermissionGranted(context)) {
val locationManager = context.getSystemService(LocationManager::class.java)
locationManager.lastLocation
} else {
null
}
}
/**
* Gets adaptive wallpaper image.
*/
private fun getAdaptiveWallpaperImage(context: Context, adaptiveType: AdaptiveType): Bitmap? {
try {
return BitmapFactory.decodeStream(
context.openFileInput(getAdaptiveWallpaperFilename(adaptiveType))
)
} catch (e: FileNotFoundException) {
Log.e(TAG, "Failed to get adaptive wallpaper image.", e)
}
return null
}
}