1 /* 2 * Copyright 2024 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.android.photopicker.core.theme 18 19 import android.content.Intent 20 import android.os.Bundle 21 import android.provider.MediaStore 22 import androidx.compose.ui.graphics.Color 23 import androidx.compose.ui.graphics.isUnspecified 24 import androidx.compose.ui.graphics.luminance 25 26 /** 27 * Holds parameters and utility methods for setting a custom picker accent color. 28 * 29 * Only valid input color codes with luminance greater than 0.6 will be set on major picker 30 * components. 31 * 32 * This feature can only be used for photo picker opened in [MediaStore#ACTION_PICK_IMAGES] mode. 33 */ 34 class AccentColorHelper(val inputColor: Long) { 35 36 companion object { 37 38 /** 39 * Creates a [AccentColorHelper] using an Intent. This builder will try to locate an accent 40 * color long by checking the Intent's extras for a [EXTRA_PICK_IMAGES_ACCENT_COLOR] key and 41 * providing that to the AccentColorHelper's constructor. 42 * 43 * @return AccentColorHelper built from the intent's extras 44 */ withIntentnull45 fun withIntent(intent: Intent): AccentColorHelper { 46 47 val extras: Bundle? = intent.extras 48 if ( 49 extras != null && 50 extras.containsKey(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR) && 51 intent.action != MediaStore.ACTION_PICK_IMAGES 52 ) { 53 throw IllegalArgumentException( 54 "Accent color customisation is not available for ${intent.action}" 55 ) 56 } 57 val inputColor = extras?.getLong(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR, -1) ?: -1 58 return AccentColorHelper(inputColor) 59 } 60 } 61 62 private val DARK_TEXT_COLOR = "#000000" 63 private val LIGHT_TEXT_COLOR = "#FFFFFF" 64 65 private val accentColor: Color 66 private val textColorForAccentComponents: Color 67 68 init { 69 70 accentColor = checkColorValidityAndGetColor(inputColor) 71 72 // accentColor being equal to [Color.Unspecified] would mean that the color 73 // passed as an input does not satisfy the validity tests for being an accent 74 // color. 75 if (inputColor > -1 && accentColor.isUnspecified) { 76 throw IllegalArgumentException("Color not valid for accent color") 77 } 78 79 // Set the Text Color only if the accentColor is not Unspecified 80 textColorForAccentComponents = 81 if (accentColor.isUnspecified) { 82 Color.Unspecified 83 } else { 84 85 Color( 86 android.graphics.Color.parseColor( 87 if (isAccentColorBright(accentColor.luminance())) DARK_TEXT_COLOR 88 else LIGHT_TEXT_COLOR, 89 ), 90 ) 91 } 92 } 93 94 /** 95 * Checks input color validity and returns the color without alpha component if valid, or -1. 96 */ checkColorValidityAndGetColornull97 private fun checkColorValidityAndGetColor(color: Long): Color { 98 // Gives us the formatted color string where the mask gives us the color in the RRGGBB 99 // format and the %06X gives zero-padded hex (6 characters long) 100 val hexColor = String.format("#%06X", (0xFFFFFF.toLong() and color)) 101 val inputColor = android.graphics.Color.parseColor(hexColor) 102 if (!isColorFeasibleForBothBackgrounds(Color(inputColor).luminance())) { 103 return Color.Unspecified 104 } 105 return Color(inputColor) 106 } 107 108 /** 109 * Returns true if the input color is within the range of [0.05 to 0.9] so that the color works 110 * both on light and dark background. Range has been set by testing with different colors. 111 */ isColorFeasibleForBothBackgroundsnull112 private fun isColorFeasibleForBothBackgrounds(luminance: Float): Boolean { 113 return luminance >= 0.05 && luminance < 0.9 114 } 115 116 /** Indicates if the accent color is bright (luminance >= 0.6). */ isAccentColorBrightnull117 private fun isAccentColorBright(accentColorLuminance: Float): Boolean = 118 accentColorLuminance >= 0.6 119 120 /** 121 * Returns the accent color which has been passed as an input in the picker intent. 122 * 123 * Default value for this is [Color.Unspecified] 124 */ 125 fun getAccentColor(): Color { 126 return accentColor 127 } 128 129 /** 130 * Returns the appropriate text color for components using the accent color as the background 131 * which has been passed as an input in the picker intent. 132 * 133 * Default value for this is [Color.Unspecified] 134 */ getTextColorForAccentComponentsnull135 fun getTextColorForAccentComponents(): Color { 136 return textColorForAccentComponents 137 } 138 139 /** Indicates that a valid accent color is used for the photopicker theme */ isValidAccentColorSetnull140 fun isValidAccentColorSet(): Boolean { 141 return accentColor != Color.Unspecified 142 } 143 } 144