Reorganize and group modules
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
*/**/g_services.json
|
||||
@@ -0,0 +1,52 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = sdk.versions.minSdk.get().toInt()
|
||||
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
consumerProguardFiles("proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
namespace = "de.mm20.launcher2.gservices"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.google.auth)
|
||||
implementation(libs.google.apiclient)
|
||||
implementation(libs.google.drive)
|
||||
implementation(libs.google.oauth2)
|
||||
|
||||
implementation(project(":core:i18n"))
|
||||
implementation(project(":core:crashreporter"))
|
||||
}
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep class com.google.** { *; }
|
||||
@@ -0,0 +1 @@
|
||||
{"installed":{"client_id":"xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com","project_id":"xxxxx-xxxxxxxxxxxxx","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}
|
||||
@@ -0,0 +1,20 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="de.mm20.launcher2.gservices.GoogleAuthRedirectActivity"
|
||||
android:theme="@style/GoogleSigninTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:path="/google-auth-redirect"
|
||||
android:scheme="${applicationId}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.mm20.launcher2.gservices
|
||||
|
||||
import com.google.api.services.drive.model.File
|
||||
import java.util.*
|
||||
|
||||
data class DriveFile(
|
||||
val fileId : String,
|
||||
val label: String,
|
||||
val size: Long,
|
||||
val mimeType : String,
|
||||
val isDirectory : Boolean,
|
||||
val directoryColor: String?,
|
||||
val viewUri: String,
|
||||
val metadata: DriveFileMeta
|
||||
) {
|
||||
companion object {
|
||||
fun fromApiDriveFile(file: File): DriveFile {
|
||||
return DriveFile(
|
||||
fileId = file.id,
|
||||
label = file.name,
|
||||
size = file.getSize() ?: 0,
|
||||
isDirectory = file.mimeType == "application/vnd.google-apps.folder",
|
||||
mimeType = file.mimeType,
|
||||
metadata = DriveFileMeta(
|
||||
owners = file.owners?.map { it.displayName ?: it.emailAddress ?: "" } ?: emptyList(),
|
||||
width = file.imageMediaMetadata?.width ?: file.videoMediaMetadata?.width,
|
||||
height = file.imageMediaMetadata?.height ?: file.videoMediaMetadata?.height
|
||||
),
|
||||
directoryColor = file.folderColorRgb?.lowercase(Locale.ROOT),
|
||||
viewUri = file.webViewLink ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DriveFileMeta(
|
||||
val owners : List<String>,
|
||||
val width: Int?,
|
||||
val height: Int?
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.mm20.launcher2.gservices
|
||||
|
||||
data class GoogleAccount(
|
||||
val name: String
|
||||
)
|
||||
@@ -0,0 +1,251 @@
|
||||
package de.mm20.launcher2.gservices
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.browser.customtabs.*
|
||||
import androidx.core.content.edit
|
||||
import com.google.api.client.auth.oauth2.Credential
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
|
||||
import com.google.api.client.http.HttpRequestInitializer
|
||||
import com.google.api.client.http.javanet.NetHttpTransport
|
||||
import com.google.api.client.json.gson.GsonFactory
|
||||
import com.google.api.client.util.store.FileDataStoreFactory
|
||||
import com.google.api.services.drive.Drive
|
||||
import com.google.api.services.oauth2.Oauth2
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class GoogleApiHelper private constructor(private val context: Context) {
|
||||
|
||||
val transport by lazy {
|
||||
NetHttpTransport()
|
||||
}
|
||||
|
||||
suspend fun queryGDriveFiles(query: String): List<DriveFile> {
|
||||
val requestInitializer = getRequestInitializer() ?: return emptyList()
|
||||
val jsonFactory = GsonFactory.getDefaultInstance()
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val drive =
|
||||
Drive.Builder(transport, jsonFactory, requestInitializer).build()
|
||||
val request = drive.files().list()
|
||||
request.q = "name contains '${query.replace("'", "")}'"
|
||||
request.pageSize = 10
|
||||
request.fields =
|
||||
"files(id, webViewLink, size, name, mimeType, owners, imageMediaMetadata, videoMediaMetadata, folderColorRgb)"
|
||||
request.corpora = "user"
|
||||
val response = request.execute()
|
||||
val files = response.files ?: return@withContext emptyList()
|
||||
files.map { DriveFile.fromApiDriveFile(it) }
|
||||
|
||||
} catch (e: IOException) {
|
||||
emptyList()
|
||||
} catch (e: Error) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getCredential(): Credential? {
|
||||
val authFlow = getAuthFlow() ?: return null
|
||||
return withContext(Dispatchers.IO) {
|
||||
val credential: Credential? = authFlow.loadCredential(USER_ID)
|
||||
if ((credential?.expiresInSeconds ?: 0) < 5 * 60) {
|
||||
try {
|
||||
if (credential?.refreshToken() == false) return@withContext null
|
||||
} catch (e: IOException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
}
|
||||
return@withContext credential
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getRequestInitializer(): HttpRequestInitializer? {
|
||||
val credential = getCredential() ?: return null
|
||||
|
||||
return HttpRequestInitializer { request ->
|
||||
credential.initialize(request)
|
||||
request?.connectTimeout = 5000
|
||||
request?.readTimeout = 10000
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAccount(): GoogleAccount? {
|
||||
|
||||
val name = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(
|
||||
PREF_ACCOUNT_NAME,
|
||||
null
|
||||
) ?: loadAccountName()
|
||||
|
||||
|
||||
return name?.let {
|
||||
GoogleAccount(name = it)
|
||||
}
|
||||
}
|
||||
|
||||
fun isAvailable(): Boolean {
|
||||
return getConfigResId() != 0
|
||||
}
|
||||
|
||||
private fun getConfigResId(): Int {
|
||||
return context.resources.getIdentifier("g_services", "raw", context.packageName)
|
||||
}
|
||||
|
||||
|
||||
private fun getAuthFlow(throwErrors: Boolean = false): GoogleAuthorizationCodeFlow? {
|
||||
val configResId = getConfigResId()
|
||||
if (configResId == 0) return null
|
||||
val jsonFactory = GsonFactory.getDefaultInstance()
|
||||
try {
|
||||
return GoogleAuthorizationCodeFlow.Builder(
|
||||
NetHttpTransport(),
|
||||
jsonFactory,
|
||||
GoogleClientSecrets.load(
|
||||
jsonFactory,
|
||||
context.resources.openRawResource(configResId).reader()
|
||||
),
|
||||
SCOPES
|
||||
)
|
||||
.setCredentialDataStore(
|
||||
FileDataStoreFactory(context.filesDir).getDataStore(
|
||||
"google_signin"
|
||||
)
|
||||
)
|
||||
.build()
|
||||
} catch (e: IOException) {
|
||||
if (throwErrors) throw e
|
||||
else {
|
||||
File(context.filesDir, "google_signin").delete()
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
|
||||
putString(PREF_ACCOUNT_NAME, null)
|
||||
}
|
||||
Log.e("MM20", "Google account has been reset because data store couldn't be readg")
|
||||
CrashReporter.logException(e)
|
||||
return getAuthFlow(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var callback: (() -> Unit)? = null
|
||||
|
||||
suspend fun login(activity: Activity) {
|
||||
val authFlow = getAuthFlow() ?: return
|
||||
|
||||
suspendCancellableCoroutine<Unit> {
|
||||
val url = authFlow
|
||||
.newAuthorizationUrl()
|
||||
.setRedirectUri(getRedirectUri())
|
||||
.toString()
|
||||
val themeColor = 0xFF4285f4.toInt()
|
||||
|
||||
val customTabsIntent = CustomTabsIntent
|
||||
.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(themeColor)
|
||||
.setNavigationBarColor(themeColor)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
callingActivity = activity.javaClass
|
||||
callback = {
|
||||
it.resumeWith(Result.success(Unit))
|
||||
}
|
||||
it.invokeOnCancellation {
|
||||
callback = null
|
||||
Log.d("MM20", "Google Signin has been canceled")
|
||||
}
|
||||
|
||||
customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
|
||||
customTabsIntent.launchUrl(activity, Uri.parse(url))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun finishAuthFlow(activity: Activity, code: String) {
|
||||
val authFlow = getAuthFlow() ?: return
|
||||
withContext(Dispatchers.IO) {
|
||||
val tokenResponse = try {
|
||||
authFlow.newTokenRequest(code).setRedirectUri(getRedirectUri()).execute()
|
||||
} catch (e: IOException) {
|
||||
CrashReporter.logException(e)
|
||||
return@withContext
|
||||
}
|
||||
authFlow.createAndStoreCredential(tokenResponse, USER_ID)
|
||||
}
|
||||
loadAccountName()
|
||||
returnToPreviousActivity(activity)
|
||||
}
|
||||
|
||||
fun cancelAuthFlow(activity: Activity) {
|
||||
returnToPreviousActivity(activity)
|
||||
}
|
||||
|
||||
private fun returnToPreviousActivity(activity: Activity) {
|
||||
val intent = Intent(activity, callingActivity)
|
||||
callingActivity = null
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
activity.startActivity(intent)
|
||||
callback?.invoke()
|
||||
}
|
||||
|
||||
private suspend fun loadAccountName(): String? {
|
||||
val requestInitializer = getRequestInitializer() ?: return null
|
||||
val jsonFactory = GsonFactory.getDefaultInstance()
|
||||
val oauth2 = Oauth2.Builder(transport, jsonFactory, requestInitializer).build()
|
||||
try {
|
||||
val meResponse = withContext(Dispatchers.IO) {
|
||||
oauth2.userinfo().v2().me().get().execute()
|
||||
}
|
||||
if (meResponse != null) {
|
||||
val name = meResponse.name
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
|
||||
putString(PREF_ACCOUNT_NAME, name)
|
||||
}
|
||||
return name
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
fun logout() {
|
||||
val authFlow = getAuthFlow() ?: return
|
||||
authFlow.credentialDataStore.clear()
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
|
||||
putString(PREF_ACCOUNT_NAME, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRedirectUri(): String {
|
||||
return "${context.packageName}:/google-auth-redirect"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var instance: GoogleApiHelper
|
||||
|
||||
fun getInstance(context: Context): GoogleApiHelper {
|
||||
if (!::instance.isInitialized) instance = GoogleApiHelper(context.applicationContext)
|
||||
return instance
|
||||
}
|
||||
|
||||
val SCOPES = setOf("https://www.googleapis.com/auth/drive.metadata.readonly", "profile")
|
||||
const val USER_ID = "google-user"
|
||||
const val PREFS = "google-account"
|
||||
const val PREF_ACCOUNT_NAME = "name"
|
||||
|
||||
private var callingActivity: Class<Activity>? = null
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package de.mm20.launcher2.gservices
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class GoogleAuthRedirectActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val gServiceHelper = GoogleApiHelper.getInstance(this)
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (code == null) {
|
||||
gServiceHelper.cancelAuthFlow(this)
|
||||
finish()
|
||||
}
|
||||
else {
|
||||
lifecycleScope.launch {
|
||||
gServiceHelper.finishAuthFlow(this@GoogleAuthRedirectActivity, code)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="46dp"
|
||||
android:height="46dp"
|
||||
android:viewportWidth="46"
|
||||
android:viewportHeight="46">
|
||||
<path
|
||||
android:pathData="m45.265,23.521c0,-1.624 -0.146,-3.186 -0.416,-4.685L23.278,18.836L23.278,27.695L35.604,27.695c-0.531,2.863 -2.145,5.288 -4.57,6.912v5.747h7.402c4.331,-3.987 6.829,-9.859 6.829,-16.834z"
|
||||
android:strokeWidth="2.54476"
|
||||
android:fillColor="#4285f4"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="m23.278,45.903c6.184,0 11.368,-2.051 15.157,-5.549l-7.402,-5.747c-2.051,1.374 -4.674,2.186 -7.756,2.186 -5.965,0 -11.014,-4.029 -12.815,-9.442h-7.652v5.934C6.58,40.77 14.325,45.903 23.278,45.903Z"
|
||||
android:strokeWidth="2.54476"
|
||||
android:fillColor="#34a853"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M10.463,27.352C10.005,25.977 9.745,24.509 9.745,23c0,-1.51 0.26,-2.977 0.718,-4.352L10.463,12.715L2.811,12.715C1.26,15.806 0.375,19.304 0.375,23c0,3.696 0.885,7.194 2.436,10.285z"
|
||||
android:strokeWidth="2.54476"
|
||||
android:fillColor="#fbbc05"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="m23.278,9.206c3.363,0 6.382,1.156 8.755,3.425l6.569,-6.569C34.636,2.367 29.451,0.097 23.278,0.097c-8.953,0 -16.698,5.132 -20.467,12.617l7.652,5.934c1.801,-5.413 6.85,-9.442 12.815,-9.442z"
|
||||
android:strokeWidth="2.54476"
|
||||
android:fillColor="#ea4335"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="GoogleSigninTheme" parent="@style/Theme.AppCompat.NoActionBar">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1 @@
|
||||
{"installed":{"client_id":"xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com","project_id":"xxxxx-xxxxxxxxxxxxx","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,43 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = sdk.versions.minSdk.get().toInt()
|
||||
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
namespace = "de.mm20.launcher2.lib.materialcolorutilities"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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 hct;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import utils.ColorUtils;
|
||||
|
||||
/**
|
||||
* CAM16, a color appearance model. Colors are not just defined by their hex code, but rather, a hex
|
||||
* code and viewing conditions.
|
||||
*
|
||||
* <p>CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
|
||||
* astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and should be used when
|
||||
* measuring distances between colors.
|
||||
*
|
||||
* <p>In traditional color spaces, a color can be identified solely by the observer's measurement of
|
||||
* the color. Color appearance models such as CAM16 also use information about the environment where
|
||||
* the color was observed, known as the viewing conditions.
|
||||
*
|
||||
* <p>For example, white under the traditional assumption of a midday sun white point is accurately
|
||||
* measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
|
||||
*/
|
||||
public final class Cam16 {
|
||||
// Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16.
|
||||
static final double[][] XYZ_TO_CAM16RGB = {
|
||||
{0.401288, 0.650173, -0.051461},
|
||||
{-0.250268, 1.204414, 0.045854},
|
||||
{-0.002079, 0.048952, 0.953127}
|
||||
};
|
||||
|
||||
// Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates.
|
||||
static final double[][] CAM16RGB_TO_XYZ = {
|
||||
{1.8620678, -1.0112547, 0.14918678},
|
||||
{0.38752654, 0.62144744, -0.00897398},
|
||||
{-0.01584150, -0.03412294, 1.0499644}
|
||||
};
|
||||
|
||||
// CAM16 color dimensions, see getters for documentation.
|
||||
private final double hue;
|
||||
private final double chroma;
|
||||
private final double j;
|
||||
private final double q;
|
||||
private final double m;
|
||||
private final double s;
|
||||
|
||||
// Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*.
|
||||
private final double jstar;
|
||||
private final double astar;
|
||||
private final double bstar;
|
||||
|
||||
/**
|
||||
* CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar,
|
||||
* astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and is used to measure
|
||||
* distances between colors.
|
||||
*/
|
||||
double distance(Cam16 other) {
|
||||
double dJ = getJstar() - other.getJstar();
|
||||
double dA = getAstar() - other.getAstar();
|
||||
double dB = getBstar() - other.getBstar();
|
||||
double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB);
|
||||
double dE = 1.41 * Math.pow(dEPrime, 0.63);
|
||||
return dE;
|
||||
}
|
||||
|
||||
/** Hue in CAM16 */
|
||||
public double getHue() {
|
||||
return hue;
|
||||
}
|
||||
|
||||
/** Chroma in CAM16 */
|
||||
public double getChroma() {
|
||||
return chroma;
|
||||
}
|
||||
|
||||
/** Lightness in CAM16 */
|
||||
public double getJ() {
|
||||
return j;
|
||||
}
|
||||
|
||||
/**
|
||||
* Brightness in CAM16.
|
||||
*
|
||||
* <p>Prefer lightness, brightness is an absolute quantity. For example, a sheet of white paper is
|
||||
* much brighter viewed in sunlight than in indoor light, but it is the lightest object under any
|
||||
* lighting.
|
||||
*/
|
||||
public double getQ() {
|
||||
return q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Colorfulness in CAM16.
|
||||
*
|
||||
* <p>Prefer chroma, colorfulness is an absolute quantity. For example, a yellow toy car is much
|
||||
* more colorful outside than inside, but it has the same chroma in both environments.
|
||||
*/
|
||||
public double getM() {
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saturation in CAM16.
|
||||
*
|
||||
* <p>Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness
|
||||
* relative to the color's own brightness, where chroma is colorfulness relative to white.
|
||||
*/
|
||||
public double getS() {
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Lightness coordinate in CAM16-UCS */
|
||||
public double getJstar() {
|
||||
return jstar;
|
||||
}
|
||||
|
||||
/** a* coordinate in CAM16-UCS */
|
||||
public double getAstar() {
|
||||
return astar;
|
||||
}
|
||||
|
||||
/** b* coordinate in CAM16-UCS */
|
||||
public double getBstar() {
|
||||
return bstar;
|
||||
}
|
||||
|
||||
/**
|
||||
* All of the CAM16 dimensions can be calculated from 3 of the dimensions, in the following
|
||||
* combinations: - {j or q} and {c, m, or s} and hue - jstar, astar, bstar Prefer using a static
|
||||
* method that constructs from 3 of those dimensions. This constructor is intended for those
|
||||
* methods to use to return all possible dimensions.
|
||||
*
|
||||
* @param hue for example, red, orange, yellow, green, etc.
|
||||
* @param chroma informally, colorfulness / color intensity. like saturation in HSL, except
|
||||
* perceptually accurate.
|
||||
* @param j lightness
|
||||
* @param q brightness; ratio of lightness to white point's lightness
|
||||
* @param m colorfulness
|
||||
* @param s saturation; ratio of chroma to white point's chroma
|
||||
* @param jstar CAM16-UCS J coordinate
|
||||
* @param astar CAM16-UCS a coordinate
|
||||
* @param bstar CAM16-UCS b coordinate
|
||||
*/
|
||||
private Cam16(
|
||||
double hue,
|
||||
double chroma,
|
||||
double j,
|
||||
double q,
|
||||
double m,
|
||||
double s,
|
||||
double jstar,
|
||||
double astar,
|
||||
double bstar) {
|
||||
this.hue = hue;
|
||||
this.chroma = chroma;
|
||||
this.j = j;
|
||||
this.q = q;
|
||||
this.m = m;
|
||||
this.s = s;
|
||||
this.jstar = jstar;
|
||||
this.astar = astar;
|
||||
this.bstar = bstar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CAM16 color from a color, assuming the color was viewed in default viewing conditions.
|
||||
*
|
||||
* @param argb ARGB representation of a color.
|
||||
*/
|
||||
public static Cam16 fromInt(int argb) {
|
||||
return fromIntInViewingConditions(argb, ViewingConditions.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CAM16 color from a color in defined viewing conditions.
|
||||
*
|
||||
* @param argb ARGB representation of a color.
|
||||
* @param viewingConditions Information about the environment where the color was observed.
|
||||
*/
|
||||
// The RGB => XYZ conversion matrix elements are derived scientific constants. While the values
|
||||
// may differ at runtime due to floating point imprecision, keeping the values the same, and
|
||||
// accurate, across implementations takes precedence.
|
||||
@SuppressWarnings("FloatingPointLiteralPrecision")
|
||||
static Cam16 fromIntInViewingConditions(int argb, ViewingConditions viewingConditions) {
|
||||
// Transform ARGB int to XYZ
|
||||
int red = (argb & 0x00ff0000) >> 16;
|
||||
int green = (argb & 0x0000ff00) >> 8;
|
||||
int blue = (argb & 0x000000ff);
|
||||
double redL = ColorUtils.linearized(red);
|
||||
double greenL = ColorUtils.linearized(green);
|
||||
double blueL = ColorUtils.linearized(blue);
|
||||
double x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL;
|
||||
double y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL;
|
||||
double z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL;
|
||||
|
||||
// Transform XYZ to 'cone'/'rgb' responses
|
||||
double[][] matrix = XYZ_TO_CAM16RGB;
|
||||
double rT = (x * matrix[0][0]) + (y * matrix[0][1]) + (z * matrix[0][2]);
|
||||
double gT = (x * matrix[1][0]) + (y * matrix[1][1]) + (z * matrix[1][2]);
|
||||
double bT = (x * matrix[2][0]) + (y * matrix[2][1]) + (z * matrix[2][2]);
|
||||
|
||||
// Discount illuminant
|
||||
double rD = viewingConditions.getRgbD()[0] * rT;
|
||||
double gD = viewingConditions.getRgbD()[1] * gT;
|
||||
double bD = viewingConditions.getRgbD()[2] * bT;
|
||||
|
||||
// Chromatic adaptation
|
||||
double rAF = Math.pow(viewingConditions.getFl() * Math.abs(rD) / 100.0, 0.42);
|
||||
double gAF = Math.pow(viewingConditions.getFl() * Math.abs(gD) / 100.0, 0.42);
|
||||
double bAF = Math.pow(viewingConditions.getFl() * Math.abs(bD) / 100.0, 0.42);
|
||||
double rA = Math.signum(rD) * 400.0 * rAF / (rAF + 27.13);
|
||||
double gA = Math.signum(gD) * 400.0 * gAF / (gAF + 27.13);
|
||||
double bA = Math.signum(bD) * 400.0 * bAF / (bAF + 27.13);
|
||||
|
||||
// redness-greenness
|
||||
double a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
|
||||
// yellowness-blueness
|
||||
double b = (rA + gA - 2.0 * bA) / 9.0;
|
||||
|
||||
// auxiliary components
|
||||
double u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0;
|
||||
double p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0;
|
||||
|
||||
// hue
|
||||
double atan2 = Math.atan2(b, a);
|
||||
double atanDegrees = Math.toDegrees(atan2);
|
||||
double hue =
|
||||
atanDegrees < 0
|
||||
? atanDegrees + 360.0
|
||||
: atanDegrees >= 360 ? atanDegrees - 360.0 : atanDegrees;
|
||||
double hueRadians = Math.toRadians(hue);
|
||||
|
||||
// achromatic response to color
|
||||
double ac = p2 * viewingConditions.getNbb();
|
||||
|
||||
// CAM16 lightness and brightness
|
||||
double j =
|
||||
100.0
|
||||
* Math.pow(
|
||||
ac / viewingConditions.getAw(),
|
||||
viewingConditions.getC() * viewingConditions.getZ());
|
||||
double q =
|
||||
4.0
|
||||
/ viewingConditions.getC()
|
||||
* Math.sqrt(j / 100.0)
|
||||
* (viewingConditions.getAw() + 4.0)
|
||||
* viewingConditions.getFlRoot();
|
||||
|
||||
// CAM16 chroma, colorfulness, and saturation.
|
||||
double huePrime = (hue < 20.14) ? hue + 360 : hue;
|
||||
double eHue = 0.25 * (Math.cos(Math.toRadians(huePrime) + 2.0) + 3.8);
|
||||
double p1 = 50000.0 / 13.0 * eHue * viewingConditions.getNc() * viewingConditions.getNcb();
|
||||
double t = p1 * Math.hypot(a, b) / (u + 0.305);
|
||||
double alpha =
|
||||
Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73) * Math.pow(t, 0.9);
|
||||
// CAM16 chroma, colorfulness, saturation
|
||||
double c = alpha * Math.sqrt(j / 100.0);
|
||||
double m = c * viewingConditions.getFlRoot();
|
||||
double s =
|
||||
50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
|
||||
|
||||
// CAM16-UCS components
|
||||
double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
|
||||
double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
|
||||
double astar = mstar * Math.cos(hueRadians);
|
||||
double bstar = mstar * Math.sin(hueRadians);
|
||||
|
||||
return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param j CAM16 lightness
|
||||
* @param c CAM16 chroma
|
||||
* @param h CAM16 hue
|
||||
*/
|
||||
static Cam16 fromJch(double j, double c, double h) {
|
||||
return fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param j CAM16 lightness
|
||||
* @param c CAM16 chroma
|
||||
* @param h CAM16 hue
|
||||
* @param viewingConditions Information about the environment where the color was observed.
|
||||
*/
|
||||
private static Cam16 fromJchInViewingConditions(
|
||||
double j, double c, double h, ViewingConditions viewingConditions) {
|
||||
double q =
|
||||
4.0
|
||||
/ viewingConditions.getC()
|
||||
* Math.sqrt(j / 100.0)
|
||||
* (viewingConditions.getAw() + 4.0)
|
||||
* viewingConditions.getFlRoot();
|
||||
double m = c * viewingConditions.getFlRoot();
|
||||
double alpha = c / Math.sqrt(j / 100.0);
|
||||
double s =
|
||||
50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0));
|
||||
|
||||
double hueRadians = Math.toRadians(h);
|
||||
double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j);
|
||||
double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m);
|
||||
double astar = mstar * Math.cos(hueRadians);
|
||||
double bstar = mstar * Math.sin(hueRadians);
|
||||
return new Cam16(h, c, j, q, m, s, jstar, astar, bstar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CAM16 color from CAM16-UCS coordinates.
|
||||
*
|
||||
* @param jstar CAM16-UCS lightness.
|
||||
* @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
|
||||
* axis.
|
||||
* @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
|
||||
* axis.
|
||||
*/
|
||||
public static Cam16 fromUcs(double jstar, double astar, double bstar) {
|
||||
|
||||
return fromUcsInViewingConditions(jstar, astar, bstar, ViewingConditions.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CAM16 color from CAM16-UCS coordinates in defined viewing conditions.
|
||||
*
|
||||
* @param jstar CAM16-UCS lightness.
|
||||
* @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y
|
||||
* axis.
|
||||
* @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X
|
||||
* axis.
|
||||
* @param viewingConditions Information about the environment where the color was observed.
|
||||
*/
|
||||
public static Cam16 fromUcsInViewingConditions(
|
||||
double jstar, double astar, double bstar, ViewingConditions viewingConditions) {
|
||||
|
||||
double m = Math.hypot(astar, bstar);
|
||||
double m2 = Math.expm1(m * 0.0228) / 0.0228;
|
||||
double c = m2 / viewingConditions.getFlRoot();
|
||||
double h = Math.atan2(bstar, astar) * (180.0 / Math.PI);
|
||||
if (h < 0.0) {
|
||||
h += 360.0;
|
||||
}
|
||||
double j = jstar / (1. - (jstar - 100.) * 0.007);
|
||||
return fromJchInViewingConditions(j, c, h, viewingConditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* ARGB representation of the color. Assumes the color was viewed in default viewing conditions,
|
||||
* which are near-identical to the default viewing conditions for sRGB.
|
||||
*/
|
||||
public int toInt() {
|
||||
return viewed(ViewingConditions.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* ARGB representation of the color, in defined viewing conditions.
|
||||
*
|
||||
* @param viewingConditions Information about the environment where the color will be viewed.
|
||||
* @return ARGB representation of color
|
||||
*/
|
||||
int viewed(ViewingConditions viewingConditions) {
|
||||
double alpha =
|
||||
(getChroma() == 0.0 || getJ() == 0.0) ? 0.0 : getChroma() / Math.sqrt(getJ() / 100.0);
|
||||
|
||||
double t =
|
||||
Math.pow(
|
||||
alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73), 1.0 / 0.9);
|
||||
double hRad = Math.toRadians(getHue());
|
||||
|
||||
double eHue = 0.25 * (Math.cos(hRad + 2.0) + 3.8);
|
||||
double ac =
|
||||
viewingConditions.getAw()
|
||||
* Math.pow(getJ() / 100.0, 1.0 / viewingConditions.getC() / viewingConditions.getZ());
|
||||
double p1 = eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
|
||||
double p2 = (ac / viewingConditions.getNbb());
|
||||
|
||||
double hSin = Math.sin(hRad);
|
||||
double hCos = Math.cos(hRad);
|
||||
|
||||
double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * hCos + 108.0 * t * hSin);
|
||||
double a = gamma * hCos;
|
||||
double b = gamma * hSin;
|
||||
double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
|
||||
double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
|
||||
double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
|
||||
|
||||
double rCBase = max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA)));
|
||||
double rC =
|
||||
Math.signum(rA) * (100.0 / viewingConditions.getFl()) * Math.pow(rCBase, 1.0 / 0.42);
|
||||
double gCBase = max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA)));
|
||||
double gC =
|
||||
Math.signum(gA) * (100.0 / viewingConditions.getFl()) * Math.pow(gCBase, 1.0 / 0.42);
|
||||
double bCBase = max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA)));
|
||||
double bC =
|
||||
Math.signum(bA) * (100.0 / viewingConditions.getFl()) * Math.pow(bCBase, 1.0 / 0.42);
|
||||
double rF = rC / viewingConditions.getRgbD()[0];
|
||||
double gF = gC / viewingConditions.getRgbD()[1];
|
||||
double bF = bC / viewingConditions.getRgbD()[2];
|
||||
|
||||
double[][] matrix = CAM16RGB_TO_XYZ;
|
||||
double x = (rF * matrix[0][0]) + (gF * matrix[0][1]) + (bF * matrix[0][2]);
|
||||
double y = (rF * matrix[1][0]) + (gF * matrix[1][1]) + (bF * matrix[1][2]);
|
||||
double z = (rF * matrix[2][0]) + (gF * matrix[2][1]) + (bF * matrix[2][2]);
|
||||
|
||||
return ColorUtils.argbFromXyz(x, y, z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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 hct;
|
||||
|
||||
import utils.ColorUtils;
|
||||
|
||||
/**
|
||||
* A color system built using CAM16 hue and chroma, and L* from L*a*b*.
|
||||
*
|
||||
* <p>Using L* creates a link between the color system, contrast, and thus accessibility. Contrast
|
||||
* ratio depends on relative luminance, or Y in the XYZ color space. L*, or perceptual luminance can
|
||||
* be calculated from Y.
|
||||
*
|
||||
* <p>Unlike Y, L* is linear to human perception, allowing trivial creation of accurate color tones.
|
||||
*
|
||||
* <p>Unlike contrast ratio, measuring contrast in L* is linear, and simple to calculate. A
|
||||
* difference of 40 in HCT tone guarantees a contrast ratio >= 3.0, and a difference of 50
|
||||
* guarantees a contrast ratio >= 4.5.
|
||||
*/
|
||||
|
||||
/**
|
||||
* HCT, hue, chroma, and tone. A color system that provides a perceptually accurate color
|
||||
* measurement system that can also accurately render what colors will appear as in different
|
||||
* lighting environments.
|
||||
*/
|
||||
public final class Hct {
|
||||
private double hue;
|
||||
private double chroma;
|
||||
private double tone;
|
||||
private int argb;
|
||||
|
||||
/**
|
||||
* Create an HCT color from hue, chroma, and tone.
|
||||
*
|
||||
* @param hue 0 <= hue < 360; invalid values are corrected.
|
||||
* @param chroma 0 <= chroma < ?; Informally, colorfulness. The color returned may be lower than
|
||||
* the requested chroma. Chroma has a different maximum for any given hue and tone.
|
||||
* @param tone 0 <= tone <= 100; invalid values are corrected.
|
||||
* @return HCT representation of a color in default viewing conditions.
|
||||
*/
|
||||
public static Hct from(double hue, double chroma, double tone) {
|
||||
int argb = HctSolver.solveToInt(hue, chroma, tone);
|
||||
return new Hct(argb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HCT color from a color.
|
||||
*
|
||||
* @param argb ARGB representation of a color.
|
||||
* @return HCT representation of a color in default viewing conditions
|
||||
*/
|
||||
public static Hct fromInt(int argb) {
|
||||
return new Hct(argb);
|
||||
}
|
||||
|
||||
private Hct(int argb) {
|
||||
setInternalState(argb);
|
||||
}
|
||||
|
||||
public double getHue() {
|
||||
return hue;
|
||||
}
|
||||
|
||||
public double getChroma() {
|
||||
return chroma;
|
||||
}
|
||||
|
||||
public double getTone() {
|
||||
return tone;
|
||||
}
|
||||
|
||||
public int toInt() {
|
||||
return argb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the hue of this color. Chroma may decrease because chroma has a different maximum for any
|
||||
* given hue and tone.
|
||||
*
|
||||
* @param newHue 0 <= newHue < 360; invalid values are corrected.
|
||||
*/
|
||||
public void setHue(double newHue) {
|
||||
setInternalState(HctSolver.solveToInt(newHue, chroma, tone));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the chroma of this color. Chroma may decrease because chroma has a different maximum for
|
||||
* any given hue and tone.
|
||||
*
|
||||
* @param newChroma 0 <= newChroma < ?
|
||||
*/
|
||||
public void setChroma(double newChroma) {
|
||||
setInternalState(HctSolver.solveToInt(hue, newChroma, tone));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tone of this color. Chroma may decrease because chroma has a different maximum for any
|
||||
* given hue and tone.
|
||||
*
|
||||
* @param newTone 0 <= newTone <= 100; invalid valids are corrected.
|
||||
*/
|
||||
public void setTone(double newTone) {
|
||||
setInternalState(HctSolver.solveToInt(hue, chroma, newTone));
|
||||
}
|
||||
|
||||
private void setInternalState(int argb) {
|
||||
this.argb = argb;
|
||||
Cam16 cam = Cam16.fromInt(argb);
|
||||
hue = cam.getHue();
|
||||
chroma = cam.getChroma();
|
||||
this.tone = ColorUtils.lstarFromArgb(argb);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
/*
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// This file is automatically generated. Do not modify it.
|
||||
|
||||
package hct;
|
||||
|
||||
import utils.ColorUtils;
|
||||
import utils.MathUtils;
|
||||
|
||||
/** A class that solves the HCT equation. */
|
||||
public class HctSolver {
|
||||
private HctSolver() {}
|
||||
|
||||
static final double[][] SCALED_DISCOUNT_FROM_LINRGB =
|
||||
new double[][] {
|
||||
new double[] {
|
||||
0.001200833568784504, 0.002389694492170889, 0.0002795742885861124,
|
||||
},
|
||||
new double[] {
|
||||
0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398,
|
||||
},
|
||||
new double[] {
|
||||
0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076,
|
||||
},
|
||||
};
|
||||
|
||||
static final double[][] LINRGB_FROM_SCALED_DISCOUNT =
|
||||
new double[][] {
|
||||
new double[] {
|
||||
1373.2198709594231, -1100.4251190754821, -7.278681089101213,
|
||||
},
|
||||
new double[] {
|
||||
-271.815969077903, 559.6580465940733, -32.46047482791194,
|
||||
},
|
||||
new double[] {
|
||||
1.9622899599665666, -57.173814538844006, 308.7233197812385,
|
||||
},
|
||||
};
|
||||
|
||||
static final double[] Y_FROM_LINRGB = new double[] {0.2126, 0.7152, 0.0722};
|
||||
|
||||
static final double[] CRITICAL_PLANES =
|
||||
new double[] {
|
||||
0.015176349177441876,
|
||||
0.045529047532325624,
|
||||
0.07588174588720938,
|
||||
0.10623444424209313,
|
||||
0.13658714259697685,
|
||||
0.16693984095186062,
|
||||
0.19729253930674434,
|
||||
0.2276452376616281,
|
||||
0.2579979360165119,
|
||||
0.28835063437139563,
|
||||
0.3188300904430532,
|
||||
0.350925934958123,
|
||||
0.3848314933096426,
|
||||
0.42057480301049466,
|
||||
0.458183274052838,
|
||||
0.4976837250274023,
|
||||
0.5391024159806381,
|
||||
0.5824650784040898,
|
||||
0.6277969426914107,
|
||||
0.6751227633498623,
|
||||
0.7244668422128921,
|
||||
0.775853049866786,
|
||||
0.829304845476233,
|
||||
0.8848452951698498,
|
||||
0.942497089126609,
|
||||
1.0022825574869039,
|
||||
1.0642236851973577,
|
||||
1.1283421258858297,
|
||||
1.1946592148522128,
|
||||
1.2631959812511864,
|
||||
1.3339731595349034,
|
||||
1.407011200216447,
|
||||
1.4823302800086415,
|
||||
1.5599503113873272,
|
||||
1.6398909516233677,
|
||||
1.7221716113234105,
|
||||
1.8068114625156377,
|
||||
1.8938294463134073,
|
||||
1.9832442801866852,
|
||||
2.075074464868551,
|
||||
2.1693382909216234,
|
||||
2.2660538449872063,
|
||||
2.36523901573795,
|
||||
2.4669114995532007,
|
||||
2.5710888059345764,
|
||||
2.6777882626779785,
|
||||
2.7870270208169257,
|
||||
2.898822059350997,
|
||||
3.0131901897720907,
|
||||
3.1301480604002863,
|
||||
3.2497121605402226,
|
||||
3.3718988244681087,
|
||||
3.4967242352587946,
|
||||
3.624204428461639,
|
||||
3.754355295633311,
|
||||
3.887192587735158,
|
||||
4.022731918402185,
|
||||
4.160988767090289,
|
||||
4.301978482107941,
|
||||
4.445716283538092,
|
||||
4.592217266055746,
|
||||
4.741496401646282,
|
||||
4.893568542229298,
|
||||
5.048448422192488,
|
||||
5.20615066083972,
|
||||
5.3666897647573375,
|
||||
5.5300801301023865,
|
||||
5.696336044816294,
|
||||
5.865471690767354,
|
||||
6.037501145825082,
|
||||
6.212438385869475,
|
||||
6.390297286737924,
|
||||
6.571091626112461,
|
||||
6.7548350853498045,
|
||||
6.941541251256611,
|
||||
7.131223617812143,
|
||||
7.323895587840543,
|
||||
7.5195704746346665,
|
||||
7.7182615035334345,
|
||||
7.919981813454504,
|
||||
8.124744458384042,
|
||||
8.332562408825165,
|
||||
8.543448553206703,
|
||||
8.757415699253682,
|
||||
8.974476575321063,
|
||||
9.194643831691977,
|
||||
9.417930041841839,
|
||||
9.644347703669503,
|
||||
9.873909240696694,
|
||||
10.106627003236781,
|
||||
10.342513269534024,
|
||||
10.58158024687427,
|
||||
10.8238400726681,
|
||||
11.069304815507364,
|
||||
11.317986476196008,
|
||||
11.569896988756009,
|
||||
11.825048221409341,
|
||||
12.083451977536606,
|
||||
12.345119996613247,
|
||||
12.610063955123938,
|
||||
12.878295467455942,
|
||||
13.149826086772048,
|
||||
13.42466730586372,
|
||||
13.702830557985108,
|
||||
13.984327217668513,
|
||||
14.269168601521828,
|
||||
14.55736596900856,
|
||||
14.848930523210871,
|
||||
15.143873411576273,
|
||||
15.44220572664832,
|
||||
15.743938506781891,
|
||||
16.04908273684337,
|
||||
16.35764934889634,
|
||||
16.66964922287304,
|
||||
16.985093187232053,
|
||||
17.30399201960269,
|
||||
17.62635644741625,
|
||||
17.95219714852476,
|
||||
18.281524751807332,
|
||||
18.614349837764564,
|
||||
18.95068293910138,
|
||||
19.290534541298456,
|
||||
19.633915083172692,
|
||||
19.98083495742689,
|
||||
20.331304511189067,
|
||||
20.685334046541502,
|
||||
21.042933821039977,
|
||||
21.404114048223256,
|
||||
21.76888489811322,
|
||||
22.137256497705877,
|
||||
22.50923893145328,
|
||||
22.884842241736916,
|
||||
23.264076429332462,
|
||||
23.6469514538663,
|
||||
24.033477234264016,
|
||||
24.42366364919083,
|
||||
24.817520537484558,
|
||||
25.21505769858089,
|
||||
25.61628489293138,
|
||||
26.021211842414342,
|
||||
26.429848230738664,
|
||||
26.842203703840827,
|
||||
27.258287870275353,
|
||||
27.678110301598522,
|
||||
28.10168053274597,
|
||||
28.529008062403893,
|
||||
28.96010235337422,
|
||||
29.39497283293396,
|
||||
29.83362889318845,
|
||||
30.276079891419332,
|
||||
30.722335150426627,
|
||||
31.172403958865512,
|
||||
31.62629557157785,
|
||||
32.08401920991837,
|
||||
32.54558406207592,
|
||||
33.010999283389665,
|
||||
33.4802739966603,
|
||||
33.953417292456834,
|
||||
34.430438229418264,
|
||||
34.911345834551085,
|
||||
35.39614910352207,
|
||||
35.88485700094671,
|
||||
36.37747846067349,
|
||||
36.87402238606382,
|
||||
37.37449765026789,
|
||||
37.87891309649659,
|
||||
38.38727753828926,
|
||||
38.89959975977785,
|
||||
39.41588851594697,
|
||||
39.93615253289054,
|
||||
40.460400508064545,
|
||||
40.98864111053629,
|
||||
41.520882981230194,
|
||||
42.05713473317016,
|
||||
42.597404951718396,
|
||||
43.141702194811224,
|
||||
43.6900349931913,
|
||||
44.24241185063697,
|
||||
44.798841244188324,
|
||||
45.35933162437017,
|
||||
45.92389141541209,
|
||||
46.49252901546552,
|
||||
47.065252796817916,
|
||||
47.64207110610409,
|
||||
48.22299226451468,
|
||||
48.808024568002054,
|
||||
49.3971762874833,
|
||||
49.9904556690408,
|
||||
50.587870934119984,
|
||||
51.189430279724725,
|
||||
51.79514187861014,
|
||||
52.40501387947288,
|
||||
53.0190544071392,
|
||||
53.637271562750364,
|
||||
54.259673423945976,
|
||||
54.88626804504493,
|
||||
55.517063457223934,
|
||||
56.15206766869424,
|
||||
56.79128866487574,
|
||||
57.43473440856916,
|
||||
58.08241284012621,
|
||||
58.734331877617365,
|
||||
59.39049941699807,
|
||||
60.05092333227251,
|
||||
60.715611475655585,
|
||||
61.38457167773311,
|
||||
62.057811747619894,
|
||||
62.7353394731159,
|
||||
63.417162620860914,
|
||||
64.10328893648692,
|
||||
64.79372614476921,
|
||||
65.48848194977529,
|
||||
66.18756403501224,
|
||||
66.89098006357258,
|
||||
67.59873767827808,
|
||||
68.31084450182222,
|
||||
69.02730813691093,
|
||||
69.74813616640164,
|
||||
70.47333615344107,
|
||||
71.20291564160104,
|
||||
71.93688215501312,
|
||||
72.67524319850172,
|
||||
73.41800625771542,
|
||||
74.16517879925733,
|
||||
74.9167682708136,
|
||||
75.67278210128072,
|
||||
76.43322770089146,
|
||||
77.1981124613393,
|
||||
77.96744375590167,
|
||||
78.74122893956174,
|
||||
79.51947534912904,
|
||||
80.30219030335869,
|
||||
81.08938110306934,
|
||||
81.88105503125999,
|
||||
82.67721935322541,
|
||||
83.4778813166706,
|
||||
84.28304815182372,
|
||||
85.09272707154808,
|
||||
85.90692527145302,
|
||||
86.72564993000343,
|
||||
87.54890820862819,
|
||||
88.3767072518277,
|
||||
89.2090541872801,
|
||||
90.04595612594655,
|
||||
90.88742016217518,
|
||||
91.73345337380438,
|
||||
92.58406282226491,
|
||||
93.43925555268066,
|
||||
94.29903859396902,
|
||||
95.16341895893969,
|
||||
96.03240364439274,
|
||||
96.9059996312159,
|
||||
97.78421388448044,
|
||||
98.6670533535366,
|
||||
99.55452497210776,
|
||||
};
|
||||
|
||||
/**
|
||||
* Sanitizes a small enough angle in radians.
|
||||
*
|
||||
* @param angle An angle in radians; must not deviate too much from 0.
|
||||
* @return A coterminal angle between 0 and 2pi.
|
||||
*/
|
||||
static double sanitizeRadians(double angle) {
|
||||
return (angle + Math.PI * 8) % (Math.PI * 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delinearizes an RGB component, returning a floating-point number.
|
||||
*
|
||||
* @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
|
||||
* @return 0.0 <= output <= 255.0, color channel converted to regular RGB space
|
||||
*/
|
||||
static double trueDelinearized(double rgbComponent) {
|
||||
double normalized = rgbComponent / 100.0;
|
||||
double delinearized = 0.0;
|
||||
if (normalized <= 0.0031308) {
|
||||
delinearized = normalized * 12.92;
|
||||
} else {
|
||||
delinearized = 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
|
||||
}
|
||||
return delinearized * 255.0;
|
||||
}
|
||||
|
||||
static double chromaticAdaptation(double component) {
|
||||
double af = Math.pow(Math.abs(component), 0.42);
|
||||
return MathUtils.signum(component) * 400.0 * af / (af + 27.13);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hue of a linear RGB color in CAM16.
|
||||
*
|
||||
* @param linrgb The linear RGB coordinates of a color.
|
||||
* @return The hue of the color in CAM16, in radians.
|
||||
*/
|
||||
static double hueOf(double[] linrgb) {
|
||||
double[] scaledDiscount = MathUtils.matrixMultiply(linrgb, SCALED_DISCOUNT_FROM_LINRGB);
|
||||
double rA = chromaticAdaptation(scaledDiscount[0]);
|
||||
double gA = chromaticAdaptation(scaledDiscount[1]);
|
||||
double bA = chromaticAdaptation(scaledDiscount[2]);
|
||||
// redness-greenness
|
||||
double a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
|
||||
// yellowness-blueness
|
||||
double b = (rA + gA - 2.0 * bA) / 9.0;
|
||||
return Math.atan2(b, a);
|
||||
}
|
||||
|
||||
static boolean areInCyclicOrder(double a, double b, double c) {
|
||||
double deltaAB = sanitizeRadians(b - a);
|
||||
double deltaAC = sanitizeRadians(c - a);
|
||||
return deltaAB < deltaAC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Solves the lerp equation.
|
||||
*
|
||||
* @param source The starting number.
|
||||
* @param mid The number in the middle.
|
||||
* @param target The ending number.
|
||||
* @return A number t such that lerp(source, target, t) = mid.
|
||||
*/
|
||||
static double intercept(double source, double mid, double target) {
|
||||
return (mid - source) / (target - source);
|
||||
}
|
||||
|
||||
static double[] lerpPoint(double[] source, double t, double[] target) {
|
||||
return new double[] {
|
||||
source[0] + (target[0] - source[0]) * t,
|
||||
source[1] + (target[1] - source[1]) * t,
|
||||
source[2] + (target[2] - source[2]) * t,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Intersects a segment with a plane.
|
||||
*
|
||||
* @param source The coordinates of point A.
|
||||
* @param coordinate The R-, G-, or B-coordinate of the plane.
|
||||
* @param target The coordinates of point B.
|
||||
* @param axis The axis the plane is perpendicular with. (0: R, 1: G, 2: B)
|
||||
* @return The intersection point of the segment AB with the plane R=coordinate, G=coordinate, or
|
||||
* B=coordinate
|
||||
*/
|
||||
static double[] setCoordinate(double[] source, double coordinate, double[] target, int axis) {
|
||||
double t = intercept(source[axis], coordinate, target[axis]);
|
||||
return lerpPoint(source, t, target);
|
||||
}
|
||||
|
||||
static boolean isBounded(double x) {
|
||||
return 0.0 <= x && x <= 100.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the nth possible vertex of the polygonal intersection.
|
||||
*
|
||||
* @param y The Y value of the plane.
|
||||
* @param n The zero-based index of the point. 0 <= n <= 11.
|
||||
* @return The nth possible vertex of the polygonal intersection of the y plane and the RGB cube,
|
||||
* in linear RGB coordinates, if it exists. If this possible vertex lies outside of the cube,
|
||||
* [-1.0, -1.0, -1.0] is returned.
|
||||
*/
|
||||
static double[] nthVertex(double y, int n) {
|
||||
double kR = Y_FROM_LINRGB[0];
|
||||
double kG = Y_FROM_LINRGB[1];
|
||||
double kB = Y_FROM_LINRGB[2];
|
||||
double coordA = n % 4 <= 1 ? 0.0 : 100.0;
|
||||
double coordB = n % 2 == 0 ? 0.0 : 100.0;
|
||||
if (n < 4) {
|
||||
double g = coordA;
|
||||
double b = coordB;
|
||||
double r = (y - g * kG - b * kB) / kR;
|
||||
if (isBounded(r)) {
|
||||
return new double[] {r, g, b};
|
||||
} else {
|
||||
return new double[] {-1.0, -1.0, -1.0};
|
||||
}
|
||||
} else if (n < 8) {
|
||||
double b = coordA;
|
||||
double r = coordB;
|
||||
double g = (y - r * kR - b * kB) / kG;
|
||||
if (isBounded(g)) {
|
||||
return new double[] {r, g, b};
|
||||
} else {
|
||||
return new double[] {-1.0, -1.0, -1.0};
|
||||
}
|
||||
} else {
|
||||
double r = coordA;
|
||||
double g = coordB;
|
||||
double b = (y - r * kR - g * kG) / kB;
|
||||
if (isBounded(b)) {
|
||||
return new double[] {r, g, b};
|
||||
} else {
|
||||
return new double[] {-1.0, -1.0, -1.0};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the segment containing the desired color.
|
||||
*
|
||||
* @param y The Y value of the color.
|
||||
* @param targetHue The hue of the color.
|
||||
* @return A list of two sets of linear RGB coordinates, each corresponding to an endpoint of the
|
||||
* segment containing the desired color.
|
||||
*/
|
||||
static double[][] bisectToSegment(double y, double targetHue) {
|
||||
double[] left = new double[] {-1.0, -1.0, -1.0};
|
||||
double[] right = left;
|
||||
double leftHue = 0.0;
|
||||
double rightHue = 0.0;
|
||||
boolean initialized = false;
|
||||
boolean uncut = true;
|
||||
for (int n = 0; n < 12; n++) {
|
||||
double[] mid = nthVertex(y, n);
|
||||
if (mid[0] < 0) {
|
||||
continue;
|
||||
}
|
||||
double midHue = hueOf(mid);
|
||||
if (!initialized) {
|
||||
left = mid;
|
||||
right = mid;
|
||||
leftHue = midHue;
|
||||
rightHue = midHue;
|
||||
initialized = true;
|
||||
continue;
|
||||
}
|
||||
if (uncut || areInCyclicOrder(leftHue, midHue, rightHue)) {
|
||||
uncut = false;
|
||||
if (areInCyclicOrder(leftHue, targetHue, midHue)) {
|
||||
right = mid;
|
||||
rightHue = midHue;
|
||||
} else {
|
||||
left = mid;
|
||||
leftHue = midHue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new double[][] {left, right};
|
||||
}
|
||||
|
||||
static double[] midpoint(double[] a, double[] b) {
|
||||
return new double[] {
|
||||
(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
static int criticalPlaneBelow(double x) {
|
||||
return (int) Math.floor(x - 0.5);
|
||||
}
|
||||
|
||||
static int criticalPlaneAbove(double x) {
|
||||
return (int) Math.ceil(x - 0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a color with the given Y and hue on the boundary of the cube.
|
||||
*
|
||||
* @param y The Y value of the color.
|
||||
* @param targetHue The hue of the color.
|
||||
* @return The desired color, in linear RGB coordinates.
|
||||
*/
|
||||
static double[] bisectToLimit(double y, double targetHue) {
|
||||
double[][] segment = bisectToSegment(y, targetHue);
|
||||
double[] left = segment[0];
|
||||
double leftHue = hueOf(left);
|
||||
double[] right = segment[1];
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
if (left[axis] != right[axis]) {
|
||||
int lPlane = -1;
|
||||
int rPlane = 255;
|
||||
if (left[axis] < right[axis]) {
|
||||
lPlane = criticalPlaneBelow(trueDelinearized(left[axis]));
|
||||
rPlane = criticalPlaneAbove(trueDelinearized(right[axis]));
|
||||
} else {
|
||||
lPlane = criticalPlaneAbove(trueDelinearized(left[axis]));
|
||||
rPlane = criticalPlaneBelow(trueDelinearized(right[axis]));
|
||||
}
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (Math.abs(rPlane - lPlane) <= 1) {
|
||||
break;
|
||||
} else {
|
||||
int mPlane = (int) Math.floor((lPlane + rPlane) / 2.0);
|
||||
double midPlaneCoordinate = CRITICAL_PLANES[mPlane];
|
||||
double[] mid = setCoordinate(left, midPlaneCoordinate, right, axis);
|
||||
double midHue = hueOf(mid);
|
||||
if (areInCyclicOrder(leftHue, targetHue, midHue)) {
|
||||
right = mid;
|
||||
rPlane = mPlane;
|
||||
} else {
|
||||
left = mid;
|
||||
leftHue = midHue;
|
||||
lPlane = mPlane;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return midpoint(left, right);
|
||||
}
|
||||
|
||||
static double inverseChromaticAdaptation(double adapted) {
|
||||
double adaptedAbs = Math.abs(adapted);
|
||||
double base = Math.max(0, 27.13 * adaptedAbs / (400.0 - adaptedAbs));
|
||||
return MathUtils.signum(adapted) * Math.pow(base, 1.0 / 0.42);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a color with the given hue, chroma, and Y.
|
||||
*
|
||||
* @param hueRadians The desired hue in radians.
|
||||
* @param chroma The desired chroma.
|
||||
* @param y The desired Y.
|
||||
* @return The desired color as a hexadecimal integer, if found; 0 otherwise.
|
||||
*/
|
||||
static int findResultByJ(double hueRadians, double chroma, double y) {
|
||||
// Initial estimate of j.
|
||||
double j = Math.sqrt(y) * 11.0;
|
||||
// ===========================================================
|
||||
// Operations inlined from Cam16 to avoid repeated calculation
|
||||
// ===========================================================
|
||||
ViewingConditions viewingConditions = ViewingConditions.DEFAULT;
|
||||
double tInnerCoeff = 1 / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73);
|
||||
double eHue = 0.25 * (Math.cos(hueRadians + 2.0) + 3.8);
|
||||
double p1 = eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb();
|
||||
double hSin = Math.sin(hueRadians);
|
||||
double hCos = Math.cos(hueRadians);
|
||||
for (int iterationRound = 0; iterationRound < 5; iterationRound++) {
|
||||
// ===========================================================
|
||||
// Operations inlined from Cam16 to avoid repeated calculation
|
||||
// ===========================================================
|
||||
double jNormalized = j / 100.0;
|
||||
double alpha = chroma == 0.0 || j == 0.0 ? 0.0 : chroma / Math.sqrt(jNormalized);
|
||||
double t = Math.pow(alpha * tInnerCoeff, 1.0 / 0.9);
|
||||
double ac =
|
||||
viewingConditions.getAw()
|
||||
* Math.pow(jNormalized, 1.0 / viewingConditions.getC() / viewingConditions.getZ());
|
||||
double p2 = ac / viewingConditions.getNbb();
|
||||
double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin);
|
||||
double a = gamma * hCos;
|
||||
double b = gamma * hSin;
|
||||
double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
|
||||
double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
|
||||
double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
|
||||
double rCScaled = inverseChromaticAdaptation(rA);
|
||||
double gCScaled = inverseChromaticAdaptation(gA);
|
||||
double bCScaled = inverseChromaticAdaptation(bA);
|
||||
double[] linrgb =
|
||||
MathUtils.matrixMultiply(
|
||||
new double[] {rCScaled, gCScaled, bCScaled}, LINRGB_FROM_SCALED_DISCOUNT);
|
||||
// ===========================================================
|
||||
// Operations inlined from Cam16 to avoid repeated calculation
|
||||
// ===========================================================
|
||||
if (linrgb[0] < 0 || linrgb[1] < 0 || linrgb[2] < 0) {
|
||||
return 0;
|
||||
}
|
||||
double kR = Y_FROM_LINRGB[0];
|
||||
double kG = Y_FROM_LINRGB[1];
|
||||
double kB = Y_FROM_LINRGB[2];
|
||||
double fnj = kR * linrgb[0] + kG * linrgb[1] + kB * linrgb[2];
|
||||
if (fnj <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (iterationRound == 4 || Math.abs(fnj - y) < 0.002) {
|
||||
if (linrgb[0] > 100.01 || linrgb[1] > 100.01 || linrgb[2] > 100.01) {
|
||||
return 0;
|
||||
}
|
||||
return ColorUtils.argbFromLinrgb(linrgb);
|
||||
}
|
||||
// Iterates with Newton method,
|
||||
// Using 2 * fn(j) / j as the approximation of fn'(j)
|
||||
j = j - (fnj - y) * j / (2 * fnj);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an sRGB color with the given hue, chroma, and L*, if possible.
|
||||
*
|
||||
* @param hueDegrees The desired hue, in degrees.
|
||||
* @param chroma The desired chroma.
|
||||
* @param lstar The desired L*.
|
||||
* @return A hexadecimal representing the sRGB color. The color has sufficiently close hue,
|
||||
* chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
|
||||
* sufficiently close, and chroma will be maximized.
|
||||
*/
|
||||
public static int solveToInt(double hueDegrees, double chroma, double lstar) {
|
||||
if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) {
|
||||
return ColorUtils.argbFromLstar(lstar);
|
||||
}
|
||||
hueDegrees = MathUtils.sanitizeDegreesDouble(hueDegrees);
|
||||
double hueRadians = hueDegrees / 180 * Math.PI;
|
||||
double y = ColorUtils.yFromLstar(lstar);
|
||||
int exactAnswer = findResultByJ(hueRadians, chroma, y);
|
||||
if (exactAnswer != 0) {
|
||||
return exactAnswer;
|
||||
}
|
||||
double[] linrgb = bisectToLimit(y, hueRadians);
|
||||
return ColorUtils.argbFromLinrgb(linrgb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an sRGB color with the given hue, chroma, and L*, if possible.
|
||||
*
|
||||
* @param hueDegrees The desired hue, in degrees.
|
||||
* @param chroma The desired chroma.
|
||||
* @param lstar The desired L*.
|
||||
* @return An CAM16 object representing the sRGB color. The color has sufficiently close hue,
|
||||
* chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
|
||||
* sufficiently close, and chroma will be maximized.
|
||||
*/
|
||||
public static Cam16 solveToCam(double hueDegrees, double chroma, double lstar) {
|
||||
return Cam16.fromInt(solveToInt(hueDegrees, chroma, lstar));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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 hct;
|
||||
|
||||
import utils.ColorUtils;
|
||||
import utils.MathUtils;
|
||||
|
||||
/**
|
||||
* In traditional color spaces, a color can be identified solely by the observer's measurement of
|
||||
* the color. Color appearance models such as CAM16 also use information about the environment where
|
||||
* the color was observed, known as the viewing conditions.
|
||||
*
|
||||
* <p>For example, white under the traditional assumption of a midday sun white point is accurately
|
||||
* measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100)
|
||||
*
|
||||
* <p>This class caches intermediate values of the CAM16 conversion process that depend only on
|
||||
* viewing conditions, enabling speed ups.
|
||||
*/
|
||||
public final class ViewingConditions {
|
||||
/** sRGB-like viewing conditions. */
|
||||
public static final ViewingConditions DEFAULT =
|
||||
ViewingConditions.make(
|
||||
new double[] {
|
||||
ColorUtils.whitePointD65()[0],
|
||||
ColorUtils.whitePointD65()[1],
|
||||
ColorUtils.whitePointD65()[2]
|
||||
},
|
||||
(200.0 / Math.PI * ColorUtils.yFromLstar(50.0) / 100.f),
|
||||
50.0,
|
||||
2.0,
|
||||
false);
|
||||
|
||||
private final double aw;
|
||||
private final double nbb;
|
||||
private final double ncb;
|
||||
private final double c;
|
||||
private final double nc;
|
||||
private final double n;
|
||||
private final double[] rgbD;
|
||||
private final double fl;
|
||||
private final double flRoot;
|
||||
private final double z;
|
||||
|
||||
public double getAw() {
|
||||
return aw;
|
||||
}
|
||||
|
||||
public double getN() {
|
||||
return n;
|
||||
}
|
||||
|
||||
public double getNbb() {
|
||||
return nbb;
|
||||
}
|
||||
|
||||
double getNcb() {
|
||||
return ncb;
|
||||
}
|
||||
|
||||
double getC() {
|
||||
return c;
|
||||
}
|
||||
|
||||
double getNc() {
|
||||
return nc;
|
||||
}
|
||||
|
||||
public double[] getRgbD() {
|
||||
return rgbD;
|
||||
}
|
||||
|
||||
double getFl() {
|
||||
return fl;
|
||||
}
|
||||
|
||||
public double getFlRoot() {
|
||||
return flRoot;
|
||||
}
|
||||
|
||||
double getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ViewingConditions from a simple, physically relevant, set of parameters.
|
||||
*
|
||||
* @param whitePoint White point, measured in the XYZ color space. default = D65, or sunny day
|
||||
* afternoon
|
||||
* @param adaptingLuminance The luminance of the adapting field. Informally, how bright it is in
|
||||
* the room where the color is viewed. Can be calculated from lux by multiplying lux by
|
||||
* 0.0586. default = 11.72, or 200 lux.
|
||||
* @param backgroundLstar The lightness of the area surrounding the color. measured by L* in
|
||||
* L*a*b*. default = 50.0
|
||||
* @param surround A general description of the lighting surrounding the color. 0 is pitch dark,
|
||||
* like watching a movie in a theater. 1.0 is a dimly light room, like watching TV at home at
|
||||
* night. 2.0 means there is no difference between the lighting on the color and around it.
|
||||
* default = 2.0
|
||||
* @param discountingIlluminant Whether the eye accounts for the tint of the ambient lighting,
|
||||
* such as knowing an apple is still red in green light. default = false, the eye does not
|
||||
* perform this process on self-luminous objects like displays.
|
||||
*/
|
||||
static ViewingConditions make(
|
||||
double[] whitePoint,
|
||||
double adaptingLuminance,
|
||||
double backgroundLstar,
|
||||
double surround,
|
||||
boolean discountingIlluminant) {
|
||||
// Transform white point XYZ to 'cone'/'rgb' responses
|
||||
double[][] matrix = Cam16.XYZ_TO_CAM16RGB;
|
||||
double[] xyz = whitePoint;
|
||||
double rW = (xyz[0] * matrix[0][0]) + (xyz[1] * matrix[0][1]) + (xyz[2] * matrix[0][2]);
|
||||
double gW = (xyz[0] * matrix[1][0]) + (xyz[1] * matrix[1][1]) + (xyz[2] * matrix[1][2]);
|
||||
double bW = (xyz[0] * matrix[2][0]) + (xyz[1] * matrix[2][1]) + (xyz[2] * matrix[2][2]);
|
||||
double f = 0.8 + (surround / 10.0);
|
||||
double c =
|
||||
(f >= 0.9)
|
||||
? MathUtils.lerp(0.59, 0.69, ((f - 0.9) * 10.0))
|
||||
: MathUtils.lerp(0.525, 0.59, ((f - 0.8) * 10.0));
|
||||
double d =
|
||||
discountingIlluminant
|
||||
? 1.0
|
||||
: f * (1.0 - ((1.0 / 3.6) * Math.exp((-adaptingLuminance - 42.0) / 92.0)));
|
||||
d = MathUtils.clampDouble(0.0, 1.0, d);
|
||||
double nc = f;
|
||||
double[] rgbD =
|
||||
new double[] {
|
||||
d * (100.0 / rW) + 1.0 - d, d * (100.0 / gW) + 1.0 - d, d * (100.0 / bW) + 1.0 - d
|
||||
};
|
||||
double k = 1.0 / (5.0 * adaptingLuminance + 1.0);
|
||||
double k4 = k * k * k * k;
|
||||
double k4F = 1.0 - k4;
|
||||
double fl = (k4 * adaptingLuminance) + (0.1 * k4F * k4F * Math.cbrt(5.0 * adaptingLuminance));
|
||||
double n = (ColorUtils.yFromLstar(backgroundLstar) / whitePoint[1]);
|
||||
double z = 1.48 + Math.sqrt(n);
|
||||
double nbb = 0.725 / Math.pow(n, 0.2);
|
||||
double ncb = nbb;
|
||||
double[] rgbAFactors =
|
||||
new double[] {
|
||||
Math.pow(fl * rgbD[0] * rW / 100.0, 0.42),
|
||||
Math.pow(fl * rgbD[1] * gW / 100.0, 0.42),
|
||||
Math.pow(fl * rgbD[2] * bW / 100.0, 0.42)
|
||||
};
|
||||
|
||||
double[] rgbA =
|
||||
new double[] {
|
||||
(400.0 * rgbAFactors[0]) / (rgbAFactors[0] + 27.13),
|
||||
(400.0 * rgbAFactors[1]) / (rgbAFactors[1] + 27.13),
|
||||
(400.0 * rgbAFactors[2]) / (rgbAFactors[2] + 27.13)
|
||||
};
|
||||
|
||||
double aw = ((2.0 * rgbA[0]) + rgbA[1] + (0.05 * rgbA[2])) * nbb;
|
||||
return new ViewingConditions(n, aw, nbb, ncb, c, nc, rgbD, fl, Math.pow(fl, 0.25), z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters are intermediate values of the CAM16 conversion process. Their names are shorthand
|
||||
* for technical color science terminology, this class would not benefit from documenting them
|
||||
* individually. A brief overview is available in the CAM16 specification, and a complete overview
|
||||
* requires a color science textbook, such as Fairchild's Color Appearance Models.
|
||||
*/
|
||||
private ViewingConditions(
|
||||
double n,
|
||||
double aw,
|
||||
double nbb,
|
||||
double ncb,
|
||||
double c,
|
||||
double nc,
|
||||
double[] rgbD,
|
||||
double fl,
|
||||
double flRoot,
|
||||
double z) {
|
||||
this.n = n;
|
||||
this.aw = aw;
|
||||
this.nbb = nbb;
|
||||
this.ncb = ncb;
|
||||
this.c = c;
|
||||
this.nc = nc;
|
||||
this.rgbD = rgbD;
|
||||
this.fl = fl;
|
||||
this.flRoot = flRoot;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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 palettes;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import hct.Hct;
|
||||
|
||||
/**
|
||||
* An intermediate concept between the key color for a UI theme, and a full color scheme. 5 sets of
|
||||
* tones are generated, all except one use the same hue as the key color, and all vary in chroma.
|
||||
*/
|
||||
public final class CorePalette {
|
||||
public TonalPalette a1;
|
||||
public TonalPalette a2;
|
||||
public TonalPalette a3;
|
||||
public TonalPalette n1;
|
||||
public TonalPalette n2;
|
||||
public TonalPalette error;
|
||||
|
||||
/**
|
||||
* Create key tones from a color.
|
||||
*
|
||||
* @param argb ARGB representation of a color
|
||||
*/
|
||||
public static CorePalette of(int argb) {
|
||||
return new CorePalette(argb, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create content key tones from a color.
|
||||
*
|
||||
* @param argb ARGB representation of a color
|
||||
*/
|
||||
public static CorePalette contentOf(int argb) {
|
||||
return new CorePalette(argb, true);
|
||||
}
|
||||
|
||||
private CorePalette(int argb, boolean isContent) {
|
||||
Hct hct = Hct.fromInt(argb);
|
||||
double hue = hct.getHue();
|
||||
double chroma = hct.getChroma();
|
||||
if (isContent) {
|
||||
this.a1 = TonalPalette.fromHueAndChroma(hue, chroma);
|
||||
this.a2 = TonalPalette.fromHueAndChroma(hue, chroma / 3.);
|
||||
this.a3 = TonalPalette.fromHueAndChroma(hue + 60., chroma / 2.);
|
||||
this.n1 = TonalPalette.fromHueAndChroma(hue, min(chroma / 12., 4.));
|
||||
this.n2 = TonalPalette.fromHueAndChroma(hue, min(chroma / 6., 8.));
|
||||
} else {
|
||||
this.a1 = TonalPalette.fromHueAndChroma(hue, max(48., chroma));
|
||||
this.a2 = TonalPalette.fromHueAndChroma(hue, 16.);
|
||||
this.a3 = TonalPalette.fromHueAndChroma(hue + 60., 24.);
|
||||
this.n1 = TonalPalette.fromHueAndChroma(hue, 4.);
|
||||
this.n2 = TonalPalette.fromHueAndChroma(hue, 8.);
|
||||
}
|
||||
this.error = TonalPalette.fromHueAndChroma(25, 84.);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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 palettes;
|
||||
|
||||
import hct.Hct;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A convenience class for retrieving colors that are constant in hue and chroma, but vary in tone.
|
||||
*/
|
||||
public final class TonalPalette {
|
||||
Map<Integer, Integer> cache;
|
||||
double hue;
|
||||
double chroma;
|
||||
|
||||
/**
|
||||
* Create tones using the HCT hue and chroma from a color.
|
||||
*
|
||||
* @param argb ARGB representation of a color
|
||||
* @return Tones matching that color's hue and chroma.
|
||||
*/
|
||||
public static final TonalPalette fromInt(int argb) {
|
||||
Hct hct = Hct.fromInt(argb);
|
||||
return TonalPalette.fromHueAndChroma(hct.getHue(), hct.getChroma());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tones from a defined HCT hue and chroma.
|
||||
*
|
||||
* @param hue HCT hue
|
||||
* @param chroma HCT chroma
|
||||
* @return Tones matching hue and chroma.
|
||||
*/
|
||||
public static final TonalPalette fromHueAndChroma(double hue, double chroma) {
|
||||
return new TonalPalette(hue, chroma);
|
||||
}
|
||||
|
||||
private TonalPalette(double hue, double chroma) {
|
||||
cache = new HashMap<>();
|
||||
this.hue = hue;
|
||||
this.chroma = chroma;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an ARGB color with HCT hue and chroma of this Tones instance, and the provided HCT tone.
|
||||
*
|
||||
* @param tone HCT tone, measured from 0 to 100.
|
||||
* @return ARGB representation of a color with that tone.
|
||||
*/
|
||||
// AndroidJdkLibsChecker is higher priority than ComputeIfAbsentUseValue (b/119581923)
|
||||
@SuppressWarnings("ComputeIfAbsentUseValue")
|
||||
public int tone(int tone) {
|
||||
Integer color = cache.get(tone);
|
||||
if (color == null) {
|
||||
color = Hct.from(this.hue, this.chroma, tone).toInt();
|
||||
cache.put(tone, color);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,781 @@
|
||||
/*
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// This file is automatically generated. Do not modify it.
|
||||
|
||||
package scheme;
|
||||
|
||||
import palettes.CorePalette;
|
||||
|
||||
/** Represents a Material color scheme, a mapping of color roles to colors. */
|
||||
public class Scheme {
|
||||
private int primary;
|
||||
private int onPrimary;
|
||||
private int primaryContainer;
|
||||
private int onPrimaryContainer;
|
||||
private int secondary;
|
||||
private int onSecondary;
|
||||
private int secondaryContainer;
|
||||
private int onSecondaryContainer;
|
||||
private int tertiary;
|
||||
private int onTertiary;
|
||||
private int tertiaryContainer;
|
||||
private int onTertiaryContainer;
|
||||
private int error;
|
||||
private int onError;
|
||||
private int errorContainer;
|
||||
private int onErrorContainer;
|
||||
private int background;
|
||||
private int onBackground;
|
||||
private int surface;
|
||||
private int onSurface;
|
||||
private int surfaceVariant;
|
||||
private int onSurfaceVariant;
|
||||
private int outline;
|
||||
private int outlineVariant;
|
||||
private int shadow;
|
||||
private int scrim;
|
||||
private int inverseSurface;
|
||||
private int inverseOnSurface;
|
||||
private int inversePrimary;
|
||||
|
||||
public Scheme() {}
|
||||
|
||||
public Scheme(
|
||||
int primary,
|
||||
int onPrimary,
|
||||
int primaryContainer,
|
||||
int onPrimaryContainer,
|
||||
int secondary,
|
||||
int onSecondary,
|
||||
int secondaryContainer,
|
||||
int onSecondaryContainer,
|
||||
int tertiary,
|
||||
int onTertiary,
|
||||
int tertiaryContainer,
|
||||
int onTertiaryContainer,
|
||||
int error,
|
||||
int onError,
|
||||
int errorContainer,
|
||||
int onErrorContainer,
|
||||
int background,
|
||||
int onBackground,
|
||||
int surface,
|
||||
int onSurface,
|
||||
int surfaceVariant,
|
||||
int onSurfaceVariant,
|
||||
int outline,
|
||||
int outlineVariant,
|
||||
int shadow,
|
||||
int scrim,
|
||||
int inverseSurface,
|
||||
int inverseOnSurface,
|
||||
int inversePrimary) {
|
||||
super();
|
||||
this.primary = primary;
|
||||
this.onPrimary = onPrimary;
|
||||
this.primaryContainer = primaryContainer;
|
||||
this.onPrimaryContainer = onPrimaryContainer;
|
||||
this.secondary = secondary;
|
||||
this.onSecondary = onSecondary;
|
||||
this.secondaryContainer = secondaryContainer;
|
||||
this.onSecondaryContainer = onSecondaryContainer;
|
||||
this.tertiary = tertiary;
|
||||
this.onTertiary = onTertiary;
|
||||
this.tertiaryContainer = tertiaryContainer;
|
||||
this.onTertiaryContainer = onTertiaryContainer;
|
||||
this.error = error;
|
||||
this.onError = onError;
|
||||
this.errorContainer = errorContainer;
|
||||
this.onErrorContainer = onErrorContainer;
|
||||
this.background = background;
|
||||
this.onBackground = onBackground;
|
||||
this.surface = surface;
|
||||
this.onSurface = onSurface;
|
||||
this.surfaceVariant = surfaceVariant;
|
||||
this.onSurfaceVariant = onSurfaceVariant;
|
||||
this.outline = outline;
|
||||
this.outlineVariant = outlineVariant;
|
||||
this.shadow = shadow;
|
||||
this.scrim = scrim;
|
||||
this.inverseSurface = inverseSurface;
|
||||
this.inverseOnSurface = inverseOnSurface;
|
||||
this.inversePrimary = inversePrimary;
|
||||
}
|
||||
|
||||
public static Scheme light(int argb) {
|
||||
return lightFromCorePalette(CorePalette.of(argb));
|
||||
}
|
||||
|
||||
public static Scheme dark(int argb) {
|
||||
return darkFromCorePalette(CorePalette.of(argb));
|
||||
}
|
||||
|
||||
public static Scheme lightContent(int argb) {
|
||||
return lightFromCorePalette(CorePalette.contentOf(argb));
|
||||
}
|
||||
|
||||
public static Scheme darkContent(int argb) {
|
||||
return darkFromCorePalette(CorePalette.contentOf(argb));
|
||||
}
|
||||
|
||||
private static Scheme lightFromCorePalette(CorePalette core) {
|
||||
return new Scheme()
|
||||
.withPrimary(core.a1.tone(40))
|
||||
.withOnPrimary(core.a1.tone(100))
|
||||
.withPrimaryContainer(core.a1.tone(90))
|
||||
.withOnPrimaryContainer(core.a1.tone(10))
|
||||
.withSecondary(core.a2.tone(40))
|
||||
.withOnSecondary(core.a2.tone(100))
|
||||
.withSecondaryContainer(core.a2.tone(90))
|
||||
.withOnSecondaryContainer(core.a2.tone(10))
|
||||
.withTertiary(core.a3.tone(40))
|
||||
.withOnTertiary(core.a3.tone(100))
|
||||
.withTertiaryContainer(core.a3.tone(90))
|
||||
.withOnTertiaryContainer(core.a3.tone(10))
|
||||
.withError(core.error.tone(40))
|
||||
.withOnError(core.error.tone(100))
|
||||
.withErrorContainer(core.error.tone(90))
|
||||
.withOnErrorContainer(core.error.tone(10))
|
||||
.withBackground(core.n1.tone(99))
|
||||
.withOnBackground(core.n1.tone(10))
|
||||
.withSurface(core.n1.tone(99))
|
||||
.withOnSurface(core.n1.tone(10))
|
||||
.withSurfaceVariant(core.n2.tone(90))
|
||||
.withOnSurfaceVariant(core.n2.tone(30))
|
||||
.withOutline(core.n2.tone(50))
|
||||
.withOutlineVariant(core.n2.tone(80))
|
||||
.withShadow(core.n1.tone(0))
|
||||
.withScrim(core.n1.tone(0))
|
||||
.withInverseSurface(core.n1.tone(20))
|
||||
.withInverseOnSurface(core.n1.tone(95))
|
||||
.withInversePrimary(core.a1.tone(80));
|
||||
}
|
||||
|
||||
private static Scheme darkFromCorePalette(CorePalette core) {
|
||||
return new Scheme()
|
||||
.withPrimary(core.a1.tone(80))
|
||||
.withOnPrimary(core.a1.tone(20))
|
||||
.withPrimaryContainer(core.a1.tone(30))
|
||||
.withOnPrimaryContainer(core.a1.tone(90))
|
||||
.withSecondary(core.a2.tone(80))
|
||||
.withOnSecondary(core.a2.tone(20))
|
||||
.withSecondaryContainer(core.a2.tone(30))
|
||||
.withOnSecondaryContainer(core.a2.tone(90))
|
||||
.withTertiary(core.a3.tone(80))
|
||||
.withOnTertiary(core.a3.tone(20))
|
||||
.withTertiaryContainer(core.a3.tone(30))
|
||||
.withOnTertiaryContainer(core.a3.tone(90))
|
||||
.withError(core.error.tone(80))
|
||||
.withOnError(core.error.tone(20))
|
||||
.withErrorContainer(core.error.tone(30))
|
||||
.withOnErrorContainer(core.error.tone(80))
|
||||
.withBackground(core.n1.tone(10))
|
||||
.withOnBackground(core.n1.tone(90))
|
||||
.withSurface(core.n1.tone(10))
|
||||
.withOnSurface(core.n1.tone(90))
|
||||
.withSurfaceVariant(core.n2.tone(30))
|
||||
.withOnSurfaceVariant(core.n2.tone(80))
|
||||
.withOutline(core.n2.tone(60))
|
||||
.withOutlineVariant(core.n2.tone(30))
|
||||
.withShadow(core.n1.tone(0))
|
||||
.withScrim(core.n1.tone(0))
|
||||
.withInverseSurface(core.n1.tone(90))
|
||||
.withInverseOnSurface(core.n1.tone(20))
|
||||
.withInversePrimary(core.a1.tone(40));
|
||||
}
|
||||
|
||||
public int getPrimary() {
|
||||
return primary;
|
||||
}
|
||||
|
||||
public void setPrimary(int primary) {
|
||||
this.primary = primary;
|
||||
}
|
||||
|
||||
public Scheme withPrimary(int primary) {
|
||||
this.primary = primary;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOnPrimary() {
|
||||
return onPrimary;
|
||||
}
|
||||
|
||||
public void setOnPrimary(int onPrimary) {
|
||||
this.onPrimary = onPrimary;
|
||||
}
|
||||
|
||||
public Scheme withOnPrimary(int onPrimary) {
|
||||
this.onPrimary = onPrimary;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getPrimaryContainer() {
|
||||
return primaryContainer;
|
||||
}
|
||||
|
||||
public void setPrimaryContainer(int primaryContainer) {
|
||||
this.primaryContainer = primaryContainer;
|
||||
}
|
||||
|
||||
public Scheme withPrimaryContainer(int primaryContainer) {
|
||||
this.primaryContainer = primaryContainer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOnPrimaryContainer() {
|
||||
return onPrimaryContainer;
|
||||
}
|
||||
|
||||
public void setOnPrimaryContainer(int onPrimaryContainer) {
|
||||
this.onPrimaryContainer = onPrimaryContainer;
|
||||
}
|
||||
|
||||
public Scheme withOnPrimaryContainer(int onPrimaryContainer) {
|
||||
this.onPrimaryContainer = onPrimaryContainer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getSecondary() {
|
||||
return secondary;
|
||||
}
|
||||
|
||||
public void setSecondary(int secondary) {
|
||||
this.secondary = secondary;
|
||||
}
|
||||
|
||||
public Scheme withSecondary(int secondary) {
|
||||
this.secondary = secondary;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOnSecondary() {
|
||||
return onSecondary;
|
||||
}
|
||||
|
||||
public void setOnSecondary(int onSecondary) {
|
||||
this.onSecondary = onSecondary;
|
||||
}
|
||||
|
||||
public Scheme withOnSecondary(int onSecondary) {
|
||||
this.onSecondary = onSecondary;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getSecondaryContainer() {
|
||||
return secondaryContainer;
|
||||
}
|
||||
|
||||
public void setSecondaryContainer(int secondaryContainer) {
|
||||
this.secondaryContainer = secondaryContainer;
|
||||
}
|
||||
|
||||
public Scheme withSecondaryContainer(int secondaryContainer) {
|
||||
this.secondaryContainer = secondaryContainer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOnSecondaryContainer() {
|
||||
return onSecondaryContainer;
|
||||
}
|
||||
|
||||
public void setOnSecondaryContainer(int onSecondaryContainer) {
|
||||
this.onSecondaryContainer = onSecondaryContainer;
|
||||
}
|
||||
|
||||
public Scheme withOnSecondaryContainer(int onSecondaryContainer) {
|
||||
this.onSecondaryContainer = onSecondaryContainer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getTertiary() {
|
||||
return tertiary;
|
||||
}
|
||||
|
||||
public void setTertiary(int tertiary) {
|
||||
this.tertiary = tertiary;
|
||||
}
|
||||
|
||||
public Scheme withTertiary(int tertiary) {
|
||||
this.tertiary = tertiary;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOnTertiary() {
|
||||
return onTertiary;
|
||||
}
|
||||
|
||||
public void setOnTertiary(int onTertiary) {
|
||||
this.onTertiary = onTertiary;
|
||||
}
|
||||
|
||||
public Scheme withOnTertiary(int onTertiary) {
|
||||
this.onTertiary = onTertiary;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getTertiaryContainer() {
|
||||
return tertiaryContainer;
|
||||
}
|
||||
|
||||
public void setTertiaryContainer(int tertiaryContainer) {
|
||||
this.tertiaryContainer = tertiaryContainer;
|
||||
}
|
||||
|
||||
public Scheme withTertiaryContainer(int tertiaryContainer) {
|
||||
this.tertiaryContainer = tertiaryContainer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOnTertiaryContainer() {
|
||||
return onTertiaryContainer;
|
||||
}
|
||||
|
||||
public void setOnTertiaryContainer(int onTertiaryContainer) {
|
||||
this.onTertiaryContainer = onTertiaryContainer;
|
||||
}
|
||||
|
||||
public Scheme withOnTertiaryContainer(int onTertiaryContainer) {
|
||||
this.onTertiaryContainer = onTertiaryContainer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public void setError(int error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public Scheme withError(int error) {
|
||||
this.error = error;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOnError() {
|
||||
return onError;
|
||||
}
|
||||
|
||||
public void setOnError(int onError) {
|
||||
this.onError = onError;
|
||||
}
|
||||
|
||||
public Scheme withOnError(int onError) {
|
||||
this.onError = onError;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getErrorContainer() {
|
||||
return errorContainer;
|
||||
}
|
||||
|
||||
public void setErrorContainer(int errorContainer) {
|
||||
this.errorContainer = errorContainer;
|
||||
}
|
||||
|
||||
public Scheme withErrorContainer(int errorContainer) {
|
||||
this.errorContainer = errorContainer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOnErrorContainer() {
|
||||
return onErrorContainer;
|
||||
}
|
||||
|
||||
public void setOnErrorContainer(int onErrorContainer) {
|
||||
this.onErrorContainer = onErrorContainer;
|
||||
}
|
||||
|
||||
public Scheme withOnErrorContainer(int onErrorContainer) {
|
||||
this.onErrorContainer = onErrorContainer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getBackground() {
|
||||
return background;
|
||||
}
|
||||
|
||||
public void setBackground(int background) {
|
||||
this.background = background;
|
||||
}
|
||||
|
||||
public Scheme withBackground(int background) {
|
||||
this.background = background;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOnBackground() {
|
||||
return onBackground;
|
||||
}
|
||||
|
||||
public void setOnBackground(int onBackground) {
|
||||
this.onBackground = onBackground;
|
||||
}
|
||||
|
||||
public Scheme withOnBackground(int onBackground) {
|
||||
this.onBackground = onBackground;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getSurface() {
|
||||
return surface;
|
||||
}
|
||||
|
||||
public void setSurface(int surface) {
|
||||
this.surface = surface;
|
||||
}
|
||||
|
||||
public Scheme withSurface(int surface) {
|
||||
this.surface = surface;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOnSurface() {
|
||||
return onSurface;
|
||||
}
|
||||
|
||||
public void setOnSurface(int onSurface) {
|
||||
this.onSurface = onSurface;
|
||||
}
|
||||
|
||||
public Scheme withOnSurface(int onSurface) {
|
||||
this.onSurface = onSurface;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getSurfaceVariant() {
|
||||
return surfaceVariant;
|
||||
}
|
||||
|
||||
public void setSurfaceVariant(int surfaceVariant) {
|
||||
this.surfaceVariant = surfaceVariant;
|
||||
}
|
||||
|
||||
public Scheme withSurfaceVariant(int surfaceVariant) {
|
||||
this.surfaceVariant = surfaceVariant;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOnSurfaceVariant() {
|
||||
return onSurfaceVariant;
|
||||
}
|
||||
|
||||
public void setOnSurfaceVariant(int onSurfaceVariant) {
|
||||
this.onSurfaceVariant = onSurfaceVariant;
|
||||
}
|
||||
|
||||
public Scheme withOnSurfaceVariant(int onSurfaceVariant) {
|
||||
this.onSurfaceVariant = onSurfaceVariant;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOutline() {
|
||||
return outline;
|
||||
}
|
||||
|
||||
public void setOutline(int outline) {
|
||||
this.outline = outline;
|
||||
}
|
||||
|
||||
public Scheme withOutline(int outline) {
|
||||
this.outline = outline;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getOutlineVariant() {
|
||||
return outlineVariant;
|
||||
}
|
||||
|
||||
public void setOutlineVariant(int outlineVariant) {
|
||||
this.outlineVariant = outlineVariant;
|
||||
}
|
||||
|
||||
public Scheme withOutlineVariant(int outlineVariant) {
|
||||
this.outlineVariant = outlineVariant;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getShadow() {
|
||||
return shadow;
|
||||
}
|
||||
|
||||
public void setShadow(int shadow) {
|
||||
this.shadow = shadow;
|
||||
}
|
||||
|
||||
public Scheme withShadow(int shadow) {
|
||||
this.shadow = shadow;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getScrim() {
|
||||
return scrim;
|
||||
}
|
||||
|
||||
public void setScrim(int scrim) {
|
||||
this.scrim = scrim;
|
||||
}
|
||||
|
||||
public Scheme withScrim(int scrim) {
|
||||
this.scrim = scrim;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getInverseSurface() {
|
||||
return inverseSurface;
|
||||
}
|
||||
|
||||
public void setInverseSurface(int inverseSurface) {
|
||||
this.inverseSurface = inverseSurface;
|
||||
}
|
||||
|
||||
public Scheme withInverseSurface(int inverseSurface) {
|
||||
this.inverseSurface = inverseSurface;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getInverseOnSurface() {
|
||||
return inverseOnSurface;
|
||||
}
|
||||
|
||||
public void setInverseOnSurface(int inverseOnSurface) {
|
||||
this.inverseOnSurface = inverseOnSurface;
|
||||
}
|
||||
|
||||
public Scheme withInverseOnSurface(int inverseOnSurface) {
|
||||
this.inverseOnSurface = inverseOnSurface;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getInversePrimary() {
|
||||
return inversePrimary;
|
||||
}
|
||||
|
||||
public void setInversePrimary(int inversePrimary) {
|
||||
this.inversePrimary = inversePrimary;
|
||||
}
|
||||
|
||||
public Scheme withInversePrimary(int inversePrimary) {
|
||||
this.inversePrimary = inversePrimary;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Scheme{"
|
||||
+ "primary="
|
||||
+ primary
|
||||
+ ", onPrimary="
|
||||
+ onPrimary
|
||||
+ ", primaryContainer="
|
||||
+ primaryContainer
|
||||
+ ", onPrimaryContainer="
|
||||
+ onPrimaryContainer
|
||||
+ ", secondary="
|
||||
+ secondary
|
||||
+ ", onSecondary="
|
||||
+ onSecondary
|
||||
+ ", secondaryContainer="
|
||||
+ secondaryContainer
|
||||
+ ", onSecondaryContainer="
|
||||
+ onSecondaryContainer
|
||||
+ ", tertiary="
|
||||
+ tertiary
|
||||
+ ", onTertiary="
|
||||
+ onTertiary
|
||||
+ ", tertiaryContainer="
|
||||
+ tertiaryContainer
|
||||
+ ", onTertiaryContainer="
|
||||
+ onTertiaryContainer
|
||||
+ ", error="
|
||||
+ error
|
||||
+ ", onError="
|
||||
+ onError
|
||||
+ ", errorContainer="
|
||||
+ errorContainer
|
||||
+ ", onErrorContainer="
|
||||
+ onErrorContainer
|
||||
+ ", background="
|
||||
+ background
|
||||
+ ", onBackground="
|
||||
+ onBackground
|
||||
+ ", surface="
|
||||
+ surface
|
||||
+ ", onSurface="
|
||||
+ onSurface
|
||||
+ ", surfaceVariant="
|
||||
+ surfaceVariant
|
||||
+ ", onSurfaceVariant="
|
||||
+ onSurfaceVariant
|
||||
+ ", outline="
|
||||
+ outline
|
||||
+ ", outlineVariant="
|
||||
+ outlineVariant
|
||||
+ ", shadow="
|
||||
+ shadow
|
||||
+ ", scrim="
|
||||
+ scrim
|
||||
+ ", inverseSurface="
|
||||
+ inverseSurface
|
||||
+ ", inverseOnSurface="
|
||||
+ inverseOnSurface
|
||||
+ ", inversePrimary="
|
||||
+ inversePrimary
|
||||
+ '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
if (!(object instanceof Scheme)) {
|
||||
return false;
|
||||
}
|
||||
if (!super.equals(object)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Scheme scheme = (Scheme) object;
|
||||
|
||||
if (primary != scheme.primary) {
|
||||
return false;
|
||||
}
|
||||
if (onPrimary != scheme.onPrimary) {
|
||||
return false;
|
||||
}
|
||||
if (primaryContainer != scheme.primaryContainer) {
|
||||
return false;
|
||||
}
|
||||
if (onPrimaryContainer != scheme.onPrimaryContainer) {
|
||||
return false;
|
||||
}
|
||||
if (secondary != scheme.secondary) {
|
||||
return false;
|
||||
}
|
||||
if (onSecondary != scheme.onSecondary) {
|
||||
return false;
|
||||
}
|
||||
if (secondaryContainer != scheme.secondaryContainer) {
|
||||
return false;
|
||||
}
|
||||
if (onSecondaryContainer != scheme.onSecondaryContainer) {
|
||||
return false;
|
||||
}
|
||||
if (tertiary != scheme.tertiary) {
|
||||
return false;
|
||||
}
|
||||
if (onTertiary != scheme.onTertiary) {
|
||||
return false;
|
||||
}
|
||||
if (tertiaryContainer != scheme.tertiaryContainer) {
|
||||
return false;
|
||||
}
|
||||
if (onTertiaryContainer != scheme.onTertiaryContainer) {
|
||||
return false;
|
||||
}
|
||||
if (error != scheme.error) {
|
||||
return false;
|
||||
}
|
||||
if (onError != scheme.onError) {
|
||||
return false;
|
||||
}
|
||||
if (errorContainer != scheme.errorContainer) {
|
||||
return false;
|
||||
}
|
||||
if (onErrorContainer != scheme.onErrorContainer) {
|
||||
return false;
|
||||
}
|
||||
if (background != scheme.background) {
|
||||
return false;
|
||||
}
|
||||
if (onBackground != scheme.onBackground) {
|
||||
return false;
|
||||
}
|
||||
if (surface != scheme.surface) {
|
||||
return false;
|
||||
}
|
||||
if (onSurface != scheme.onSurface) {
|
||||
return false;
|
||||
}
|
||||
if (surfaceVariant != scheme.surfaceVariant) {
|
||||
return false;
|
||||
}
|
||||
if (onSurfaceVariant != scheme.onSurfaceVariant) {
|
||||
return false;
|
||||
}
|
||||
if (outline != scheme.outline) {
|
||||
return false;
|
||||
}
|
||||
if (outlineVariant != scheme.outlineVariant) {
|
||||
return false;
|
||||
}
|
||||
if (shadow != scheme.shadow) {
|
||||
return false;
|
||||
}
|
||||
if (scrim != scheme.scrim) {
|
||||
return false;
|
||||
}
|
||||
if (inverseSurface != scheme.inverseSurface) {
|
||||
return false;
|
||||
}
|
||||
if (inverseOnSurface != scheme.inverseOnSurface) {
|
||||
return false;
|
||||
}
|
||||
if (inversePrimary != scheme.inversePrimary) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = super.hashCode();
|
||||
result = 31 * result + primary;
|
||||
result = 31 * result + onPrimary;
|
||||
result = 31 * result + primaryContainer;
|
||||
result = 31 * result + onPrimaryContainer;
|
||||
result = 31 * result + secondary;
|
||||
result = 31 * result + onSecondary;
|
||||
result = 31 * result + secondaryContainer;
|
||||
result = 31 * result + onSecondaryContainer;
|
||||
result = 31 * result + tertiary;
|
||||
result = 31 * result + onTertiary;
|
||||
result = 31 * result + tertiaryContainer;
|
||||
result = 31 * result + onTertiaryContainer;
|
||||
result = 31 * result + error;
|
||||
result = 31 * result + onError;
|
||||
result = 31 * result + errorContainer;
|
||||
result = 31 * result + onErrorContainer;
|
||||
result = 31 * result + background;
|
||||
result = 31 * result + onBackground;
|
||||
result = 31 * result + surface;
|
||||
result = 31 * result + onSurface;
|
||||
result = 31 * result + surfaceVariant;
|
||||
result = 31 * result + onSurfaceVariant;
|
||||
result = 31 * result + outline;
|
||||
result = 31 * result + outlineVariant;
|
||||
result = 31 * result + shadow;
|
||||
result = 31 * result + scrim;
|
||||
result = 31 * result + inverseSurface;
|
||||
result = 31 * result + inverseOnSurface;
|
||||
result = 31 * result + inversePrimary;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// This file is automatically generated. Do not modify it.
|
||||
|
||||
package utils;
|
||||
|
||||
/**
|
||||
* Color science utilities.
|
||||
*
|
||||
* <p>Utility methods for color science constants and color space conversions that aren't HCT or
|
||||
* CAM16.
|
||||
*/
|
||||
public class ColorUtils {
|
||||
private ColorUtils() {}
|
||||
|
||||
static final double[][] SRGB_TO_XYZ =
|
||||
new double[][] {
|
||||
new double[] {0.41233895, 0.35762064, 0.18051042},
|
||||
new double[] {0.2126, 0.7152, 0.0722},
|
||||
new double[] {0.01932141, 0.11916382, 0.95034478},
|
||||
};
|
||||
|
||||
static final double[][] XYZ_TO_SRGB =
|
||||
new double[][] {
|
||||
new double[] {
|
||||
3.2413774792388685, -1.5376652402851851, -0.49885366846268053,
|
||||
},
|
||||
new double[] {
|
||||
-0.9691452513005321, 1.8758853451067872, 0.04156585616912061,
|
||||
},
|
||||
new double[] {
|
||||
0.05562093689691305, -0.20395524564742123, 1.0571799111220335,
|
||||
},
|
||||
};
|
||||
|
||||
static final double[] WHITE_POINT_D65 = new double[] {95.047, 100.0, 108.883};
|
||||
|
||||
/** Converts a color from RGB components to ARGB format. */
|
||||
public static int argbFromRgb(int red, int green, int blue) {
|
||||
return (255 << 24) | ((red & 255) << 16) | ((green & 255) << 8) | (blue & 255);
|
||||
}
|
||||
|
||||
/** Converts a color from linear RGB components to ARGB format. */
|
||||
public static int argbFromLinrgb(double[] linrgb) {
|
||||
int r = delinearized(linrgb[0]);
|
||||
int g = delinearized(linrgb[1]);
|
||||
int b = delinearized(linrgb[2]);
|
||||
return argbFromRgb(r, g, b);
|
||||
}
|
||||
|
||||
/** Returns the alpha component of a color in ARGB format. */
|
||||
public static int alphaFromArgb(int argb) {
|
||||
return (argb >> 24) & 255;
|
||||
}
|
||||
|
||||
/** Returns the red component of a color in ARGB format. */
|
||||
public static int redFromArgb(int argb) {
|
||||
return (argb >> 16) & 255;
|
||||
}
|
||||
|
||||
/** Returns the green component of a color in ARGB format. */
|
||||
public static int greenFromArgb(int argb) {
|
||||
return (argb >> 8) & 255;
|
||||
}
|
||||
|
||||
/** Returns the blue component of a color in ARGB format. */
|
||||
public static int blueFromArgb(int argb) {
|
||||
return argb & 255;
|
||||
}
|
||||
|
||||
/** Returns whether a color in ARGB format is opaque. */
|
||||
public static boolean isOpaque(int argb) {
|
||||
return alphaFromArgb(argb) >= 255;
|
||||
}
|
||||
|
||||
/** Converts a color from ARGB to XYZ. */
|
||||
public static int argbFromXyz(double x, double y, double z) {
|
||||
double[][] matrix = XYZ_TO_SRGB;
|
||||
double linearR = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z;
|
||||
double linearG = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z;
|
||||
double linearB = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z;
|
||||
int r = delinearized(linearR);
|
||||
int g = delinearized(linearG);
|
||||
int b = delinearized(linearB);
|
||||
return argbFromRgb(r, g, b);
|
||||
}
|
||||
|
||||
/** Converts a color from XYZ to ARGB. */
|
||||
public static double[] xyzFromArgb(int argb) {
|
||||
double r = linearized(redFromArgb(argb));
|
||||
double g = linearized(greenFromArgb(argb));
|
||||
double b = linearized(blueFromArgb(argb));
|
||||
return MathUtils.matrixMultiply(new double[] {r, g, b}, SRGB_TO_XYZ);
|
||||
}
|
||||
|
||||
/** Converts a color represented in Lab color space into an ARGB integer. */
|
||||
public static int argbFromLab(double l, double a, double b) {
|
||||
double[] whitePoint = WHITE_POINT_D65;
|
||||
double fy = (l + 16.0) / 116.0;
|
||||
double fx = a / 500.0 + fy;
|
||||
double fz = fy - b / 200.0;
|
||||
double xNormalized = labInvf(fx);
|
||||
double yNormalized = labInvf(fy);
|
||||
double zNormalized = labInvf(fz);
|
||||
double x = xNormalized * whitePoint[0];
|
||||
double y = yNormalized * whitePoint[1];
|
||||
double z = zNormalized * whitePoint[2];
|
||||
return argbFromXyz(x, y, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a color from ARGB representation to L*a*b* representation.
|
||||
*
|
||||
* @param argb the ARGB representation of a color
|
||||
* @return a Lab object representing the color
|
||||
*/
|
||||
public static double[] labFromArgb(int argb) {
|
||||
double linearR = linearized(redFromArgb(argb));
|
||||
double linearG = linearized(greenFromArgb(argb));
|
||||
double linearB = linearized(blueFromArgb(argb));
|
||||
double[][] matrix = SRGB_TO_XYZ;
|
||||
double x = matrix[0][0] * linearR + matrix[0][1] * linearG + matrix[0][2] * linearB;
|
||||
double y = matrix[1][0] * linearR + matrix[1][1] * linearG + matrix[1][2] * linearB;
|
||||
double z = matrix[2][0] * linearR + matrix[2][1] * linearG + matrix[2][2] * linearB;
|
||||
double[] whitePoint = WHITE_POINT_D65;
|
||||
double xNormalized = x / whitePoint[0];
|
||||
double yNormalized = y / whitePoint[1];
|
||||
double zNormalized = z / whitePoint[2];
|
||||
double fx = labF(xNormalized);
|
||||
double fy = labF(yNormalized);
|
||||
double fz = labF(zNormalized);
|
||||
double l = 116.0 * fy - 16;
|
||||
double a = 500.0 * (fx - fy);
|
||||
double b = 200.0 * (fy - fz);
|
||||
return new double[] {l, a, b};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an L* value to an ARGB representation.
|
||||
*
|
||||
* @param lstar L* in L*a*b*
|
||||
* @return ARGB representation of grayscale color with lightness matching L*
|
||||
*/
|
||||
public static int argbFromLstar(double lstar) {
|
||||
double y = yFromLstar(lstar);
|
||||
int component = delinearized(y);
|
||||
return argbFromRgb(component, component, component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the L* value of a color in ARGB representation.
|
||||
*
|
||||
* @param argb ARGB representation of a color
|
||||
* @return L*, from L*a*b*, coordinate of the color
|
||||
*/
|
||||
public static double lstarFromArgb(int argb) {
|
||||
double y = xyzFromArgb(argb)[1];
|
||||
return 116.0 * labF(y / 100.0) - 16.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an L* value to a Y value.
|
||||
*
|
||||
* <p>L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
|
||||
*
|
||||
* <p>L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
|
||||
* logarithmic scale.
|
||||
*
|
||||
* @param lstar L* in L*a*b*
|
||||
* @return Y in XYZ
|
||||
*/
|
||||
public static double yFromLstar(double lstar) {
|
||||
return 100.0 * labInvf((lstar + 16.0) / 116.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Linearizes an RGB component.
|
||||
*
|
||||
* @param rgbComponent 0 <= rgb_component <= 255, represents R/G/B channel
|
||||
* @return 0.0 <= output <= 100.0, color channel converted to linear RGB space
|
||||
*/
|
||||
public static double linearized(int rgbComponent) {
|
||||
double normalized = rgbComponent / 255.0;
|
||||
if (normalized <= 0.040449936) {
|
||||
return normalized / 12.92 * 100.0;
|
||||
} else {
|
||||
return Math.pow((normalized + 0.055) / 1.055, 2.4) * 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delinearizes an RGB component.
|
||||
*
|
||||
* @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
|
||||
* @return 0 <= output <= 255, color channel converted to regular RGB space
|
||||
*/
|
||||
public static int delinearized(double rgbComponent) {
|
||||
double normalized = rgbComponent / 100.0;
|
||||
double delinearized = 0.0;
|
||||
if (normalized <= 0.0031308) {
|
||||
delinearized = normalized * 12.92;
|
||||
} else {
|
||||
delinearized = 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
|
||||
}
|
||||
return MathUtils.clampInt(0, 255, (int) Math.round(delinearized * 255.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the standard white point; white on a sunny day.
|
||||
*
|
||||
* @return The white point
|
||||
*/
|
||||
public static double[] whitePointD65() {
|
||||
return WHITE_POINT_D65;
|
||||
}
|
||||
|
||||
static double labF(double t) {
|
||||
double e = 216.0 / 24389.0;
|
||||
double kappa = 24389.0 / 27.0;
|
||||
if (t > e) {
|
||||
return Math.pow(t, 1.0 / 3.0);
|
||||
} else {
|
||||
return (kappa * t + 16) / 116;
|
||||
}
|
||||
}
|
||||
|
||||
static double labInvf(double ft) {
|
||||
double e = 216.0 / 24389.0;
|
||||
double kappa = 24389.0 / 27.0;
|
||||
double ft3 = ft * ft * ft;
|
||||
if (ft3 > e) {
|
||||
return ft3;
|
||||
} else {
|
||||
return (116 * ft - 16) / kappa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// This file is automatically generated. Do not modify it.
|
||||
|
||||
package utils;
|
||||
|
||||
/** Utility methods for mathematical operations. */
|
||||
public class MathUtils {
|
||||
private MathUtils() {}
|
||||
|
||||
/**
|
||||
* The signum function.
|
||||
*
|
||||
* @return 1 if num > 0, -1 if num < 0, and 0 if num = 0
|
||||
*/
|
||||
public static int signum(double num) {
|
||||
if (num < 0) {
|
||||
return -1;
|
||||
} else if (num == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The linear interpolation function.
|
||||
*
|
||||
* @return start if amount = 0 and stop if amount = 1
|
||||
*/
|
||||
public static double lerp(double start, double stop, double amount) {
|
||||
return (1.0 - amount) * start + amount * stop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps an integer between two integers.
|
||||
*
|
||||
* @return input when min <= input <= max, and either min or max otherwise.
|
||||
*/
|
||||
public static int clampInt(int min, int max, int input) {
|
||||
if (input < min) {
|
||||
return min;
|
||||
} else if (input > max) {
|
||||
return max;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps an integer between two floating-point numbers.
|
||||
*
|
||||
* @return input when min <= input <= max, and either min or max otherwise.
|
||||
*/
|
||||
public static double clampDouble(double min, double max, double input) {
|
||||
if (input < min) {
|
||||
return min;
|
||||
} else if (input > max) {
|
||||
return max;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a degree measure as an integer.
|
||||
*
|
||||
* @return a degree measure between 0 (inclusive) and 360 (exclusive).
|
||||
*/
|
||||
public static int sanitizeDegreesInt(int degrees) {
|
||||
degrees = degrees % 360;
|
||||
if (degrees < 0) {
|
||||
degrees = degrees + 360;
|
||||
}
|
||||
return degrees;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a degree measure as a floating-point number.
|
||||
*
|
||||
* @return a degree measure between 0.0 (inclusive) and 360.0 (exclusive).
|
||||
*/
|
||||
public static double sanitizeDegreesDouble(double degrees) {
|
||||
degrees = degrees % 360.0;
|
||||
if (degrees < 0) {
|
||||
degrees = degrees + 360.0;
|
||||
}
|
||||
return degrees;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign of direction change needed to travel from one angle to another.
|
||||
*
|
||||
* <p>For angles that are 180 degrees apart from each other, both directions have the same travel
|
||||
* distance, so either direction is shortest. The value 1.0 is returned in this case.
|
||||
*
|
||||
* @param from The angle travel starts from, in degrees.
|
||||
* @param to The angle travel ends at, in degrees.
|
||||
* @return -1 if decreasing from leads to the shortest travel distance, 1 if increasing from leads
|
||||
* to the shortest travel distance.
|
||||
*/
|
||||
public static double rotationDirection(double from, double to) {
|
||||
double increasingDifference = sanitizeDegreesDouble(to - from);
|
||||
return increasingDifference <= 180.0 ? 1.0 : -1.0;
|
||||
}
|
||||
|
||||
/** Distance of two points on a circle, represented using degrees. */
|
||||
public static double differenceDegrees(double a, double b) {
|
||||
return 180.0 - Math.abs(Math.abs(a - b) - 180.0);
|
||||
}
|
||||
|
||||
/** Multiplies a 1x3 row vector with a 3x3 matrix. */
|
||||
public static double[] matrixMultiply(double[] row, double[][] matrix) {
|
||||
double a = row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2];
|
||||
double b = row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2];
|
||||
double c = row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2];
|
||||
return new double[] {a, b, c};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
*/**/msal_auth_config.json
|
||||
@@ -0,0 +1,48 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = sdk.versions.minSdk.get().toInt()
|
||||
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
consumerProguardFile("proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
namespace = "de.mm20.launcher2.msservices"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
||||
implementation(libs.microsoft.identity)
|
||||
implementation(libs.microsoft.graph)
|
||||
implementation(libs.guava)
|
||||
|
||||
implementation(project(":core:crashreporter"))
|
||||
}
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.kts.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep class com.microsoft.graph.requests.** { *; }
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"client_id" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"authorization_user_agent" : "DEFAULT",
|
||||
"account_mode": "SINGLE",
|
||||
"redirect_uri" : "msauth://de.mm20.launcher2.debug/xxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"authorities" : [
|
||||
{
|
||||
"type": "AAD",
|
||||
"audience": {
|
||||
"type": "AzureADandPersonalMicrosoftAccount",
|
||||
"tenant_id": "common"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.microsoft.identity.client.BrowserTabActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="${applicationId}"
|
||||
android:scheme="msauth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.mm20.launcher2.msservices
|
||||
|
||||
import com.microsoft.graph.models.DriveItem as MSDriveItem
|
||||
|
||||
data class DriveItem(
|
||||
val id : String,
|
||||
val label : String,
|
||||
val mimeType : String,
|
||||
val size: Long,
|
||||
val isDirectory : Boolean,
|
||||
val webUrl: String,
|
||||
val meta: DriveItemMeta
|
||||
) {
|
||||
companion object {
|
||||
fun fromApiDriveItem(driveItem: MSDriveItem) : DriveItem? {
|
||||
return DriveItem(
|
||||
id = driveItem.id ?: return null,
|
||||
label = driveItem.name ?: return null,
|
||||
mimeType = driveItem.file?.mimeType ?: "inode/directory",
|
||||
size = driveItem.size ?: 0,
|
||||
isDirectory = driveItem.file == null,
|
||||
webUrl = driveItem.webUrl ?: return null,
|
||||
meta = DriveItemMeta(
|
||||
owner = driveItem.shared?.owner?.user?.displayName,
|
||||
createdBy = driveItem.createdBy?.user?.displayName,
|
||||
width = driveItem.image?.width ?: driveItem.video?.width,
|
||||
height = driveItem.image?.height ?: driveItem.video?.height
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DriveItemMeta(
|
||||
val owner: String?,
|
||||
val createdBy: String?,
|
||||
val width: Int?,
|
||||
val height: Int?
|
||||
)
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
package de.mm20.launcher2.msservices
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import com.azure.core.credential.AccessToken
|
||||
import com.azure.core.credential.TokenCredential
|
||||
import com.microsoft.graph.authentication.TokenCredentialAuthProvider
|
||||
import com.microsoft.graph.core.ClientException
|
||||
import com.microsoft.graph.http.GraphServiceException
|
||||
import com.microsoft.graph.models.DriveSearchParameterSet
|
||||
import com.microsoft.graph.requests.GraphServiceClient
|
||||
import com.microsoft.identity.client.AuthenticationCallback
|
||||
import com.microsoft.identity.client.IAuthenticationResult
|
||||
import com.microsoft.identity.client.ISingleAccountPublicClientApplication
|
||||
import com.microsoft.identity.client.PublicClientApplication
|
||||
import com.microsoft.identity.client.exception.MsalClientException
|
||||
import com.microsoft.identity.client.exception.MsalException
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import reactor.core.publisher.Mono
|
||||
import java.net.URLEncoder
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class MicrosoftGraphApiHelper(val context: Context) {
|
||||
|
||||
private var accessToken: String? = null
|
||||
private val client: GraphServiceClient<Request> = GraphServiceClient
|
||||
.builder()
|
||||
.authenticationProvider(TokenCredentialAuthProvider {
|
||||
Mono.just(AccessToken(accessToken, null))
|
||||
})
|
||||
.buildClient()
|
||||
|
||||
private var clientApplication: ISingleAccountPublicClientApplication? = null
|
||||
|
||||
private suspend fun getClientApplication(): ISingleAccountPublicClientApplication? {
|
||||
val resId = getConfigResId()
|
||||
if (resId == 0) return null
|
||||
if (clientApplication == null) {
|
||||
clientApplication = withContext(Dispatchers.IO) {
|
||||
PublicClientApplication.createSingleAccountPublicClientApplication(
|
||||
context.applicationContext,
|
||||
resId
|
||||
)
|
||||
}
|
||||
}
|
||||
return clientApplication!!
|
||||
}
|
||||
|
||||
private suspend fun acquireAccessToken(): Boolean {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val application = getClientApplication() ?: return@withContext null
|
||||
val authority = application.configuration.defaultAuthority.authorityURL.toString()
|
||||
application.acquireTokenSilent(SCOPES, authority)
|
||||
} catch (e: MsalException) {
|
||||
CrashReporter.logException(e)
|
||||
logout()
|
||||
null
|
||||
} catch (e: ClientException) {
|
||||
CrashReporter.logException(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
accessToken = result?.accessToken
|
||||
return result != null
|
||||
}
|
||||
|
||||
suspend fun login(context: Activity) {
|
||||
val clientApplication = getClientApplication() ?: return
|
||||
suspendCoroutine<IAuthenticationResult?> {
|
||||
clientApplication.signIn(context, "", SCOPES, object : AuthenticationCallback {
|
||||
override fun onSuccess(authenticationResult: IAuthenticationResult?) {
|
||||
accessToken = authenticationResult?.accessToken
|
||||
it.resume(authenticationResult)
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
it.resume(null)
|
||||
}
|
||||
|
||||
override fun onError(exception: MsalException?) {
|
||||
if (exception != null) Log.e("MM20", exception.stackTraceToString())
|
||||
it.resume(null)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
loadAccountName()
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
accessToken = null
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
|
||||
putString(PREF_ACCOUNT_NAME, null)
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
getClientApplication()?.signOut()
|
||||
} catch (e: MsalClientException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUser(): MsUser? {
|
||||
val name = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(
|
||||
PREF_ACCOUNT_NAME,
|
||||
null
|
||||
) ?: loadAccountName()
|
||||
|
||||
|
||||
return name?.let {
|
||||
MsUser(name = it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadAccountName(): String? {
|
||||
if (!isLoggedIn()) return null
|
||||
if (!acquireAccessToken()) return null
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val user = client.me().buildRequest().get() ?: return@withContext null
|
||||
val name = user.displayName ?: user.mail ?: "Microsoft User"
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
|
||||
putString(PREF_ACCOUNT_NAME, name)
|
||||
}
|
||||
return@withContext name
|
||||
} catch (e: GraphServiceException) {
|
||||
CrashReporter.logException(e)
|
||||
logout()
|
||||
} catch (e: ClientException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun isLoggedIn(): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
getClientApplication()?.currentAccount?.currentAccount != null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun queryOneDriveFiles(query: String): List<DriveItem>? {
|
||||
if (!acquireAccessToken()) return null
|
||||
return try {
|
||||
withContext(Dispatchers.IO) {
|
||||
client.me().drive().search(
|
||||
DriveSearchParameterSet.newBuilder()
|
||||
.withQ(query)
|
||||
.build()
|
||||
)
|
||||
.buildRequest()
|
||||
.select("id,name,file,size,video,image,webUrl,shared,createdBy")
|
||||
.top(10)
|
||||
.get()
|
||||
?.currentPage
|
||||
?.mapNotNull { DriveItem.fromApiDriveItem(it) }
|
||||
}
|
||||
} catch (e: GraphServiceException) {
|
||||
CrashReporter.logException(e)
|
||||
null
|
||||
} catch (e: ClientException) {
|
||||
CrashReporter.logException(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun isAvailable(): Boolean {
|
||||
return getConfigResId() != 0
|
||||
}
|
||||
|
||||
private fun getConfigResId(): Int {
|
||||
return context.resources.getIdentifier("msal_auth_config", "raw", context.packageName)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var instance: MicrosoftGraphApiHelper
|
||||
|
||||
fun getInstance(context: Context): MicrosoftGraphApiHelper {
|
||||
if (!Companion::instance.isInitialized) instance =
|
||||
MicrosoftGraphApiHelper(context.applicationContext)
|
||||
return instance
|
||||
}
|
||||
|
||||
private val SCOPES = arrayOf(
|
||||
"User.Read",
|
||||
"Files.Read.All"
|
||||
)
|
||||
|
||||
const val PREFS = "ms-account"
|
||||
const val PREF_ACCOUNT_NAME = "name"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.mm20.launcher2.msservices
|
||||
|
||||
data class MsUser(
|
||||
val name: String
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"client_id" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"authorization_user_agent" : "DEFAULT",
|
||||
"account_mode": "SINGLE",
|
||||
"redirect_uri" : "msauth://de.mm20.launcher2.release/xxxxxxxxxxxxxxxxxx",
|
||||
"authorities" : [
|
||||
{
|
||||
"type": "AAD",
|
||||
"audience": {
|
||||
"type": "AzureADandPersonalMicrosoftAccount",
|
||||
"tenant_id": "common"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,57 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = sdk.versions.minSdk.get().toInt()
|
||||
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
namespace = "de.mm20.launcher2.nextcloud"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.materialcomponents.core)
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.securitycrypto)
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.okhttp)
|
||||
|
||||
api(project(":libs:webdav"))
|
||||
implementation(project(":core:i18n"))
|
||||
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.kts.kts.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,11 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="de.mm20.launcher2.nextcloud.LoginActivity"
|
||||
android:label="@string/preference_category_services_nextcloud"
|
||||
android:taskAffinity="de.mm20.launcher2.nextcloud"
|
||||
android:theme="@style/NextcloudLoginTheme" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,85 @@
|
||||
package de.mm20.launcher2.nextcloud
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import de.mm20.launcher2.nextcloud.databinding.ActivityNextcloudLoginBinding
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
|
||||
private val nextcloudClient = NextcloudApiHelper(this)
|
||||
|
||||
private lateinit var binding: ActivityNextcloudLoginBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityNextcloudLoginBinding.inflate(LayoutInflater.from(this))
|
||||
setContentView(binding.root)
|
||||
binding.nextButton.setOnClickListener {
|
||||
binding.serverUrlInputLayout.error = null
|
||||
lifecycleScope.launch {
|
||||
var url = binding.serverUrlInput.text.toString()
|
||||
if (!(url.startsWith("http://") || url.startsWith("https://"))) {
|
||||
url = "https://$url"
|
||||
}
|
||||
if (url.isBlank()) {
|
||||
binding.serverUrlInputLayout.error = getString(R.string.nextcloud_server_url_empty)
|
||||
return@launch
|
||||
}
|
||||
if (nextcloudClient.checkNextcloudInstallation(url)) {
|
||||
openLoginPage(url)
|
||||
} else {
|
||||
binding.serverUrlInputLayout.error = getString(R.string.nextcloud_server_invalid_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openLoginPage(url: String) {
|
||||
val webView = WebView(this)
|
||||
webView.settings.userAgentString = getString(R.string.app_name)
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||
if (request?.url?.scheme == "nc") {
|
||||
val path = request.url?.path?.trim('/') ?: run {
|
||||
setResult(0)
|
||||
finish()
|
||||
return false
|
||||
}
|
||||
val segments = path.split('&')
|
||||
var username: String? = null
|
||||
var token: String? = null
|
||||
var server: String? = null
|
||||
for (segment in segments) {
|
||||
when {
|
||||
segment.startsWith("server") -> server = segment.substringAfter(":")
|
||||
segment.startsWith("user") -> username = segment.substringAfter(":")
|
||||
segment.startsWith("password") -> token = segment.substringAfter(":")
|
||||
}
|
||||
}
|
||||
if (username != null && server != null && token != null) {
|
||||
nextcloudClient.setServer(server, username, token)
|
||||
}
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
webView.loadUrl(request?.url?.toString() ?: "")
|
||||
return false
|
||||
}
|
||||
}
|
||||
webView.settings.javaScriptEnabled = true
|
||||
setContentView(webView)
|
||||
val headers = mapOf(
|
||||
"OCS-APIREQUEST" to "true"
|
||||
)
|
||||
webView.loadUrl("$url/index.php/login/flow", headers)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.mm20.launcher2.nextcloud
|
||||
|
||||
data class NcUser(
|
||||
val displayName: String,
|
||||
val username: String
|
||||
)
|
||||
@@ -0,0 +1,204 @@
|
||||
package de.mm20.launcher2.nextcloud
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import de.mm20.launcher2.webdav.WebDavApi
|
||||
import de.mm20.launcher2.webdav.WebDavFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.*
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class NextcloudApiHelper(val context: Context) {
|
||||
|
||||
|
||||
private val httpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.authenticator(object : Authenticator {
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
if (response.priorResponse?.priorResponse != null) return null
|
||||
return response.request
|
||||
.newBuilder()
|
||||
.addHeader("Authorization", getAuthorization() ?: return null)
|
||||
.build()
|
||||
}
|
||||
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
private val preferences by lazy {
|
||||
createPreferences()
|
||||
}
|
||||
|
||||
private fun createPreferences(catchErrors: Boolean = true): SharedPreferences {
|
||||
try {
|
||||
val masterKey =
|
||||
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
||||
return EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"nextcloud",
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
if (!catchErrors) throw e
|
||||
File(context.filesDir, "../shared_prefs/nextcloud.xml").delete()
|
||||
return createPreferences(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLoginIntent(): Intent {
|
||||
return Intent(context, LoginActivity::class.java)
|
||||
}
|
||||
|
||||
fun login(activity: Activity) {
|
||||
activity.startActivity(getLoginIntent())
|
||||
}
|
||||
|
||||
|
||||
suspend fun checkNextcloudInstallation(url: String): Boolean {
|
||||
var url = url
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "https://$url"
|
||||
}
|
||||
val request = Request.Builder()
|
||||
.url("$url/remote.php/dav")
|
||||
.build()
|
||||
val response = runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.newCall(request).execute()
|
||||
}
|
||||
}.getOrNull() ?: return false
|
||||
return response.code == 200 || response.code == 401
|
||||
}
|
||||
|
||||
suspend fun getLoggedInUser(): NcUser? {
|
||||
val server = getServer()
|
||||
val username = getUserName()
|
||||
val token = getToken()
|
||||
|
||||
if (server == null || username == null || token == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val displayName = getDisplayName() ?: return null
|
||||
|
||||
return NcUser(
|
||||
displayName,
|
||||
username
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's display name or user name if the user is logged in and their token has
|
||||
* not been revoked,
|
||||
* returns null if they are not logged in.
|
||||
*/
|
||||
private suspend fun getDisplayName(): String? {
|
||||
|
||||
val displayname = preferences.getString("displayname", null)
|
||||
if (displayname != null) {
|
||||
return displayname
|
||||
}
|
||||
|
||||
val server = getServer() ?: return null
|
||||
|
||||
val request = Request.Builder()
|
||||
.addHeader("OCS-APIRequest", "true")
|
||||
.url("$server/ocs/v1.php/cloud/user?format=json")
|
||||
.build()
|
||||
|
||||
val response = runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.newCall(request).execute()
|
||||
}
|
||||
}.getOrNull() ?: return getUserName()
|
||||
|
||||
if (response.code != 200) {
|
||||
logout()
|
||||
return null
|
||||
}
|
||||
val body = response.body ?: return getUserName()
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val json = JSONObject(body.string())
|
||||
val name = json.optJSONObject("ocs")
|
||||
?.optJSONObject("data")
|
||||
?.optString("display-name")
|
||||
|
||||
preferences.edit {
|
||||
putString("displayname", name)
|
||||
}
|
||||
|
||||
return@withContext name
|
||||
?: getUserName()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAuthorization(): String? {
|
||||
return Credentials.basic(getUserName() ?: return null, getToken() ?: return null)
|
||||
}
|
||||
|
||||
fun getServer(): String? {
|
||||
return preferences.getString("server", null)
|
||||
}
|
||||
|
||||
fun getUserName(): String? {
|
||||
return preferences.getString("username", null)
|
||||
}
|
||||
|
||||
private fun getToken(): String? {
|
||||
return preferences.getString("token", null)
|
||||
}
|
||||
|
||||
internal fun setServer(server: String, username: String, token: String) {
|
||||
preferences.edit {
|
||||
putString("server", server)
|
||||
putString("username", username)
|
||||
putString("token", token)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
val server = getServer()
|
||||
val username = getUserName()
|
||||
val token = getToken()
|
||||
if (server == null || username == null || token == null) return
|
||||
val request = Request.Builder()
|
||||
.addHeader("OCS-APIREQUEST", "true")
|
||||
.delete()
|
||||
.url("$server/ocs/v2.php/core/apppassword")
|
||||
.build()
|
||||
withContext(Dispatchers.IO) {
|
||||
val response = httpClient.newCall(request).execute()
|
||||
response
|
||||
}
|
||||
preferences.edit {
|
||||
putString("server", null)
|
||||
putString("username", null)
|
||||
putString("token", null)
|
||||
putString("displayname", null)
|
||||
}
|
||||
}
|
||||
|
||||
val files by lazy {
|
||||
FilesApi()
|
||||
}
|
||||
|
||||
inner class FilesApi internal constructor() {
|
||||
suspend fun search(query: String): List<WebDavFile> {
|
||||
val server = getServer() ?: return emptyList()
|
||||
val username = getUserName() ?: return emptyList()
|
||||
return WebDavApi.search("$server/remote.php/dav/", username, query, httpClient)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<vector android:height="106.01157dp" android:viewportHeight="94.62735"
|
||||
android:viewportWidth="133.89203" android:width="150dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M67.033,10C55.228,10 45.222,18.003 42.12,28.847 39.425,23.095 33.585,19.066 26.857,19.066 17.605,19.066 10,26.671 10,35.923 10,45.175 17.605,52.783 26.857,52.783 33.585,52.783 39.425,48.751 42.12,42.999 45.222,53.843 55.228,61.849 67.033,61.849 78.751,61.849 88.706,53.964 91.886,43.242 94.631,48.864 100.4,52.783 107.031,52.783 116.283,52.783 123.892,45.175 123.892,35.923 123.892,26.671 116.283,19.066 107.031,19.066 100.4,19.066 94.631,22.983 91.886,28.604 88.706,17.882 78.751,10 67.033,10ZM67.033,19.895C75.944,19.895 83.064,27.011 83.064,35.923 83.064,44.834 75.944,51.953 67.033,51.953 58.121,51.953 51.006,44.834 51.006,35.923 51.006,27.011 58.121,19.895 67.033,19.895ZM26.857,28.961C30.761,28.961 33.822,32.018 33.822,35.923 33.822,39.827 30.761,42.888 26.857,42.888 22.953,42.888 19.896,39.827 19.896,35.923 19.896,32.018 22.953,28.961 26.857,28.961ZM107.031,28.961C110.936,28.961 113.997,32.018 113.997,35.923 113.997,39.827 110.936,42.888 107.031,42.888 103.127,42.888 100.07,39.827 100.07,35.923 100.07,32.018 103.127,28.961 107.031,28.961Z"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M39.108,73.761C41.884,73.761 43.436,75.737 43.436,78.7 43.436,78.983 43.201,79.218 42.919,79.218H35.439C35.486,81.852 37.321,83.357 39.438,83.357 40.755,83.357 41.696,82.793 42.166,82.417 42.448,82.228 42.683,82.275 42.824,82.558L42.966,82.793C43.107,83.028 43.06,83.263 42.824,83.451 42.26,83.875 41.037,84.58 39.391,84.58 36.333,84.58 33.981,82.369 33.981,79.171 34.028,75.784 36.286,73.761 39.108,73.761ZM41.978,78.183C41.884,76.019 40.567,74.937 39.061,74.937 37.321,74.937 35.816,76.066 35.486,78.183Z"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M57.562,75.267V74.091,71.645C57.562,71.315 57.75,71.127 58.079,71.127H58.456C58.785,71.127 58.926,71.315 58.926,71.645V74.091H61.043C61.372,74.091 61.56,74.279 61.56,74.608V74.749C61.56,75.079 61.372,75.22 61.043,75.22H58.926V80.394C58.926,82.793 60.384,83.075 61.184,83.122 61.607,83.169 61.748,83.263 61.748,83.64V83.922C61.748,84.251 61.607,84.392 61.184,84.392 58.926,84.392 57.562,83.028 57.562,80.582Z"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M68.334,73.761C70.121,73.761 71.25,74.514 71.767,74.937 72.003,75.126 72.05,75.361 71.814,75.643L71.673,75.878C71.485,76.16 71.25,76.16 70.968,75.972 70.497,75.643 69.604,75.031 68.381,75.031 66.123,75.031 64.335,76.725 64.335,79.218 64.335,81.664 66.123,83.357 68.381,83.357 69.839,83.357 70.827,82.699 71.297,82.275 71.579,82.087 71.767,82.134 71.956,82.417L72.097,82.605C72.238,82.887 72.191,83.075 71.956,83.31 71.438,83.734 70.168,84.627 68.287,84.627 65.229,84.627 62.877,82.417 62.877,79.218 62.924,76.019 65.276,73.761 68.334,73.761Z"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M74.59,70.422C74.59,70.092 74.402,69.904 74.731,69.904H75.107C75.436,69.904 75.954,70.092 75.954,70.422V81.664C75.954,82.981 76.565,83.122 77.036,83.169 77.271,83.169 77.459,83.31 77.459,83.64V83.969C77.459,84.298 77.318,84.486 76.942,84.486 76.095,84.486 74.59,84.204 74.59,81.946Z"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M84.233,73.761C87.243,73.761 89.689,76.066 89.689,79.124 89.689,82.228 87.243,84.58 84.233,84.58 81.222,84.58 78.776,82.228 78.776,79.124 78.776,76.066 81.222,73.761 84.233,73.761ZM84.233,83.357C86.443,83.357 88.231,81.57 88.231,79.124 88.231,76.772 86.443,75.031 84.233,75.031 82.022,75.031 80.187,76.819 80.187,79.124 80.234,81.523 82.022,83.357 84.233,83.357Z"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M107.705,73.761C110.198,73.761 111.092,75.831 111.092,75.831H111.139C111.139,75.831 111.092,75.502 111.092,75.031V70.375C111.092,70.045 110.951,69.857 111.28,69.857H111.656C111.985,69.857 112.503,70.045 112.503,70.375V83.781C112.503,84.11 112.362,84.298 112.032,84.298H111.703C111.374,84.298 111.186,84.157 111.186,83.828V83.028C111.186,82.652 111.28,82.369 111.28,82.369H111.233C111.233,82.369 110.339,84.533 107.658,84.533 104.883,84.533 103.142,82.322 103.142,79.124 103.048,75.925 104.977,73.761 107.705,73.761ZM107.752,83.357C109.492,83.357 111.092,82.134 111.092,79.171 111.092,77.054 110.01,75.031 107.799,75.031 105.964,75.031 104.459,76.537 104.459,79.171 104.506,81.711 105.823,83.357 107.752,83.357Z"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M21.86,84.345H22.236C22.565,84.345 22.753,84.157 22.753,83.828V73.727C22.753,72.128 24.494,70.986 26.469,70.986 28.445,70.986 30.185,72.128 30.185,73.727V83.828C30.185,84.157 30.374,84.345 30.703,84.345H31.079C31.408,84.345 31.55,84.157 31.55,83.828V73.667C31.55,70.986 28.868,69.669 26.422,69.669V69.669,69.669 69.669,69.669C24.07,69.669 21.389,70.986 21.389,73.667V83.828C21.389,84.157 21.53,84.345 21.86,84.345Z"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M100.367,73.997H99.991C99.661,73.997 99.473,74.185 99.473,74.514V80.206C99.473,81.805 98.438,83.263 96.416,83.263 94.44,83.263 93.358,81.805 93.358,80.206V74.514C93.358,74.185 93.17,73.997 92.841,73.997H92.464C92.135,73.997 91.994,74.185 91.994,74.514V80.582C91.994,83.263 93.97,84.58 96.416,84.58V84.58C96.416,84.58 96.416,84.58 96.416,84.58 96.416,84.58 96.416,84.58 96.416,84.58V84.58C98.862,84.58 100.837,83.263 100.837,80.582V74.514C100.884,74.185 100.696,73.997 100.367,73.997Z"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#0082c9" android:pathData="M53.803,73.919C53.687,73.938 53.577,74.015 53.471,74.141L51.567,76.41 50.142,78.108 47.985,75.536 46.815,74.141C46.709,74.015 46.589,73.946 46.465,73.935 46.34,73.924 46.211,73.972 46.085,74.077L45.797,74.319C45.545,74.531 45.558,74.765 45.769,75.017L47.674,77.287 49.253,79.168 46.941,81.923C46.939,81.925 46.938,81.928 46.936,81.93L45.769,83.32C45.558,83.572 45.581,83.838 45.834,84.049L46.122,84.29C46.374,84.502 46.603,84.448 46.815,84.196L48.718,81.927 50.143,80.229 52.3,82.801C52.302,82.802 52.304,82.804 52.305,82.805L53.472,84.196C53.684,84.449 53.948,84.471 54.2,84.26L54.489,84.018C54.741,83.806 54.728,83.572 54.516,83.32L52.612,81.051 51.033,79.169 53.345,76.414C53.347,76.412 53.348,76.409 53.35,76.407L54.516,75.017C54.728,74.765 54.704,74.5 54.452,74.288L54.164,74.046C54.038,73.94 53.918,73.901 53.803,73.919Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/serverUrlView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="32dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_nextcloud_logo" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/serverUrlInputLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:hint="@string/nextcloud_server_url"
|
||||
app:helperTextEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/serverUrlInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/nextButton"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_flow_continue" />
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="NextcloudLoginTheme" parent="Theme.MaterialComponents.NoActionBar">
|
||||
<item name="colorAccent">#0082c9</item>
|
||||
<item name="colorPrimary">#0082c9</item>
|
||||
<item name="colorPrimaryDark">@color/settings_color_primary_dark</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="NextcloudLoginTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
<item name="colorAccent">#0082c9</item>
|
||||
<item name="colorPrimary">#0082c9</item>
|
||||
<item name="colorPrimaryDark">@color/settings_color_primary_dark</item>
|
||||
<item name="android:navigationBarColor">@color/settings_color_primary_dark</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,59 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = sdk.versions.minSdk.get().toInt()
|
||||
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
namespace = "de.mm20.launcher2.owncloud"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.materialcomponents.core)
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.securitycrypto)
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.okhttp)
|
||||
|
||||
api(project(":libs:webdav"))
|
||||
implementation(project(":core:crashreporter"))
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:i18n"))
|
||||
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.kts.kts.kts.kts.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,16 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:label="@string/preference_category_services_nextcloud"
|
||||
android:taskAffinity="de.mm20.launcher2.nextcloud"
|
||||
android:parentActivityName="de.mm20.launcher2.ui.settings.SettingsActivity"
|
||||
android:theme="@style/OwncloudLoginTheme" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="de.mm20.launcher2.ui.settings.SettingsActivity" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,65 @@
|
||||
package de.mm20.launcher2.owncloud
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import de.mm20.launcher2.owncloud.databinding.ActivityOwncloudLoginBinding
|
||||
import de.mm20.launcher2.owncloud.databinding.ActivityOwncloudLoginUsernamePasswordBinding
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
|
||||
private val owncloudClient = OwncloudClient(this)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityOwncloudLoginBinding.inflate(LayoutInflater.from(this))
|
||||
setContentView(binding.root)
|
||||
binding.nextButton.setOnClickListener {
|
||||
binding.serverUrlInputLayout.error = null
|
||||
lifecycleScope.launch {
|
||||
var url = binding.serverUrlInput.text.toString()
|
||||
if (!(url.startsWith("http://") || url.startsWith("https://"))) {
|
||||
url = "https://$url"
|
||||
}
|
||||
if (url.isBlank()) {
|
||||
binding.serverUrlInputLayout.error = getString(R.string.nextcloud_server_url_empty)
|
||||
return@launch
|
||||
}
|
||||
if (owncloudClient.checkOwncloudInstallation(url)) {
|
||||
openLoginPage(url)
|
||||
} else {
|
||||
binding.serverUrlInputLayout.error = getString(R.string.owncloud_server_invalid_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openLoginPage(url: String) {
|
||||
val binding = ActivityOwncloudLoginUsernamePasswordBinding.inflate(LayoutInflater.from(this))
|
||||
setContentView(binding.root)
|
||||
binding.loginButton.setOnClickListener {
|
||||
val username = binding.username.text.toString()
|
||||
val password = binding.password.text.toString()
|
||||
if (username.isEmpty()) {
|
||||
binding.usernameInputLayout.error = getString(R.string.owncloud_username_empty)
|
||||
}
|
||||
if (password.isEmpty()) {
|
||||
binding.passwordInputLayout.error = getString(R.string.owncloud_password_empty)
|
||||
}
|
||||
if(username.isEmpty() || password.isEmpty()) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
if (owncloudClient.tryLogin(url, username, password)) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
} else {
|
||||
binding.passwordInputLayout.error = getString(R.string.owncloud_login_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.mm20.launcher2.owncloud
|
||||
|
||||
data class OcUser(
|
||||
val displayName: String,
|
||||
val username: String
|
||||
)
|
||||
@@ -0,0 +1,201 @@
|
||||
package de.mm20.launcher2.owncloud
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import androidx.security.crypto.MasterKeys
|
||||
import de.mm20.launcher2.webdav.WebDavApi
|
||||
import de.mm20.launcher2.webdav.WebDavFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.*
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class OwncloudClient(val context: Context) {
|
||||
|
||||
|
||||
private val httpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.authenticator(object : Authenticator {
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
if (response.priorResponse?.priorResponse != null) return null
|
||||
return response.request
|
||||
.newBuilder()
|
||||
.addHeader("Authorization", getAuthorization() ?: return null)
|
||||
.build()
|
||||
}
|
||||
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
private val preferences by lazy {
|
||||
createPreferences()
|
||||
}
|
||||
|
||||
private fun createPreferences(catchErrors: Boolean = true): SharedPreferences {
|
||||
try {
|
||||
val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
||||
return EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"owncloud",
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
if (!catchErrors) throw e
|
||||
File(context.filesDir, "../shared_prefs/owncloud.xml").delete()
|
||||
return createPreferences(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLoginIntent(): Intent {
|
||||
return Intent(context, LoginActivity::class.java)
|
||||
}
|
||||
|
||||
fun login(activity: Activity, requestCode: Int) {
|
||||
activity.startActivityForResult(getLoginIntent(), requestCode)
|
||||
}
|
||||
|
||||
|
||||
suspend fun checkOwncloudInstallation(url: String): Boolean {
|
||||
var url = url
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "https://$url"
|
||||
}
|
||||
val request = Request.Builder()
|
||||
.url("$url/remote.php/webdav")
|
||||
.build()
|
||||
val response = runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.newCall(request).execute()
|
||||
}
|
||||
}.getOrNull() ?: return false
|
||||
return response.code == 200 || response.code == 401
|
||||
}
|
||||
|
||||
suspend fun getLoggedInUser(): OcUser? {
|
||||
val server = getServer()
|
||||
val username = getUserName()
|
||||
val token = getToken()
|
||||
|
||||
if (server == null || username == null || token == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val displayName = getDisplayName() ?: return null
|
||||
|
||||
return OcUser(
|
||||
displayName,
|
||||
username
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's display name or user name if the user is logged in
|
||||
* returns null if they are not logged in.
|
||||
*/
|
||||
private suspend fun getDisplayName(): String? {
|
||||
|
||||
if (preferences.getString("displayname", null) != null) {
|
||||
return preferences.getString("displayname", null)
|
||||
}
|
||||
|
||||
val server = getServer() ?: return null
|
||||
|
||||
val request = Request.Builder()
|
||||
.addHeader("OCS-APIRequest", "true")
|
||||
.url("$server/ocs/v1.php/cloud/user?format=json")
|
||||
.build()
|
||||
|
||||
val response = runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.newCall(request).execute()
|
||||
}
|
||||
}.getOrNull() ?: return getUserName()
|
||||
|
||||
if (response.code != 200) {
|
||||
logout()
|
||||
return null
|
||||
}
|
||||
val body = response.body ?: return getUserName()
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val json = JSONObject(body.string())
|
||||
|
||||
return@withContext json.optJSONObject("ocs")
|
||||
?.optJSONObject("data")
|
||||
?.optString("display-name")
|
||||
?: getUserName()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAuthorization(): String? {
|
||||
return Credentials.basic(getUserName() ?: return null, getToken() ?: return null)
|
||||
}
|
||||
|
||||
fun getServer(): String? {
|
||||
return preferences.getString("server", null)
|
||||
}
|
||||
|
||||
fun getUserName(): String? {
|
||||
return preferences.getString("username", null)
|
||||
}
|
||||
|
||||
fun getUserDisplayName(): String? {
|
||||
return preferences.getString("displayname", getUserName())
|
||||
}
|
||||
|
||||
private fun getToken(): String? {
|
||||
return preferences.getString("token", null)
|
||||
}
|
||||
|
||||
internal fun setServer(server: String, username: String, token: String) {
|
||||
preferences.edit {
|
||||
putString("server", server)
|
||||
putString("username", username)
|
||||
putString("token", token)
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
preferences.edit {
|
||||
putString("server", null)
|
||||
putString("username", null)
|
||||
putString("token", null)
|
||||
putString("displayname", null)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun tryLogin(url: String, username: String, pw: String): Boolean {
|
||||
|
||||
setServer(url, username, pw)
|
||||
|
||||
val displayName = getDisplayName()
|
||||
preferences.edit {
|
||||
putString("displayname", displayName)
|
||||
}
|
||||
|
||||
return displayName != null
|
||||
}
|
||||
|
||||
val files by lazy {
|
||||
FilesApi()
|
||||
}
|
||||
|
||||
inner class FilesApi internal constructor() {
|
||||
suspend fun query(query: String): List<WebDavFile> {
|
||||
val server = getServer() ?: return emptyList()
|
||||
val username = getUserName() ?: return emptyList()
|
||||
return WebDavApi.searchReport("$server/remote.php/dav/", username, query, httpClient)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/serverUrlView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="32dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_owncloud_logo" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/serverUrlInputLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:hint="@string/owncloud_server_url"
|
||||
app:helperTextEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/serverUrlInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/nextButton"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_flow_continue" />
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/serverUrlView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="32dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_owncloud_logo" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/usernameInputLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:hint="@string/owncloud_username"
|
||||
app:helperTextEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="username"
|
||||
android:inputType="textUri" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/passwordInputLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:helperText="@string/owncloud_login_2fa_hint"
|
||||
android:hint="@string/owncloud_password">
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:inputType="textWebPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/loginButton"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_flow_login" />
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:padding="16dp"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1" />
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="OwncloudLoginTheme" parent="Theme.MaterialComponents.NoActionBar">
|
||||
<item name="colorAccent">#6d8fc0</item>
|
||||
<item name="colorPrimary">#6d8fc0</item>
|
||||
<item name="colorPrimaryDark">@color/settings_color_primary_dark</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="OwncloudLoginTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
<item name="colorAccent">#1d2d44</item>
|
||||
<item name="colorPrimary">#1d2d44</item>
|
||||
<item name="colorPrimaryDark">@color/settings_color_primary_dark</item>
|
||||
<item name="android:navigationBarColor">@color/settings_color_primary_dark</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,47 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = sdk.versions.minSdk.get().toInt()
|
||||
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
namespace = "de.mm20.launcher2.webdav"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
||||
implementation(libs.okhttp)
|
||||
|
||||
implementation(project(":core:crashreporter"))
|
||||
implementation(project(":core:ktx"))
|
||||
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.kts.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
/
|
||||
</manifest>
|
||||
@@ -0,0 +1,192 @@
|
||||
package de.mm20.launcher2.webdav
|
||||
|
||||
import com.balsikandar.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.castToOrNull
|
||||
import de.mm20.launcher2.ktx.decodeUrl
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.w3c.dom.Element
|
||||
import org.xml.sax.SAXException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.lang.Exception
|
||||
import java.net.URLDecoder
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.parsers.ParserConfigurationException
|
||||
|
||||
object WebDavApi {
|
||||
suspend fun search(webDavUrl: String, username: String, query: String, client: OkHttpClient): List<WebDavFile> {
|
||||
val requestBody = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
||||
<d:basicsearch>
|
||||
<d:select>
|
||||
<d:prop>
|
||||
<oc:fileid/>
|
||||
<d:displayname/>
|
||||
<d:getcontenttype/>
|
||||
<d:resourcetype/>
|
||||
<oc:size/>
|
||||
<oc:owner-display-name/>
|
||||
</d:prop>
|
||||
</d:select>
|
||||
<d:from>
|
||||
<d:scope>
|
||||
<d:href><![CDATA[/files/$username]]></d:href>
|
||||
<d:depth>infinity</d:depth>
|
||||
</d:scope>
|
||||
</d:from>
|
||||
<d:where>
|
||||
<d:like>
|
||||
<d:prop>
|
||||
<d:displayname/>
|
||||
</d:prop>
|
||||
<d:literal><![CDATA[%$query%]]></d:literal>
|
||||
</d:like>
|
||||
</d:where>
|
||||
<d:orderby/>
|
||||
</d:basicsearch>
|
||||
</d:searchrequest>
|
||||
""".trimIndent()
|
||||
val request = Request.Builder()
|
||||
.url(webDavUrl)
|
||||
.method("SEARCH", requestBody.toRequestBody("text/xml".toMediaType()))
|
||||
.build()
|
||||
return withContext(Dispatchers.IO) {
|
||||
val results = mutableListOf<WebDavFile>()
|
||||
try {
|
||||
val response = client.newCall(request).execute()
|
||||
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(response.body?.byteStream()
|
||||
?: return@withContext emptyList<WebDavFile>())
|
||||
val responses = document.getElementsByTagName("d:response")
|
||||
for (i in 0 until responses.length) {
|
||||
val res = responses.item(i) as? Element ?: continue
|
||||
val url = res.getElementsByTagName("d:href")
|
||||
.takeIf { it.length > 0 }?.item(0)
|
||||
?.textContent?.takeIf { it.isNotEmpty() } ?: continue
|
||||
val fileId = res.getElementsByTagName("oc:fileid")
|
||||
.takeIf { it.length > 0 }?.item(0)
|
||||
?.textContent?.toLongOrNull() ?: continue
|
||||
|
||||
val displayName = res.getElementsByTagName("d:displayname")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: url.trimEnd('/').substringAfterLast("/").decodeUrl("utf8")
|
||||
?: continue
|
||||
|
||||
val isDirectory = res.getElementsByTagName("d:resourcetype")
|
||||
.takeIf { it.length > 0 }
|
||||
?.item(0)?.childNodes?.length == 1
|
||||
val mimeType = res.getElementsByTagName("d:getcontenttype")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent?.takeIf { it.isNotEmpty() }
|
||||
?: if (isDirectory) "inode/directory" else "application/octet-stream"
|
||||
val size = res.getElementsByTagName("oc:size")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent?.toLongOrNull()
|
||||
?: 0L
|
||||
val owner = res.getElementsByTagName("oc:owner-display-name")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
|
||||
results += WebDavFile(
|
||||
name = displayName,
|
||||
id = fileId,
|
||||
isDirectory = isDirectory,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
owner = owner,
|
||||
url = url
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
return@withContext results
|
||||
}
|
||||
}
|
||||
|
||||
fun getSearchRequestBody(query: String): String {
|
||||
return """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<oc:search-files
|
||||
xmlns:a="DAV:"
|
||||
xmlns:oc="http://owncloud.org/ns">
|
||||
<a:prop>
|
||||
<oc:fileid/>
|
||||
<a:displayname/>
|
||||
<a:getcontenttype/>
|
||||
<a:resourcetype/>
|
||||
<oc:size/>
|
||||
<oc:owner-display-name/>
|
||||
</a:prop>
|
||||
<oc:search>
|
||||
<oc:pattern><![CDATA[$query]]></oc:pattern>
|
||||
<oc:limit>20</oc:limit>
|
||||
</oc:search>
|
||||
</oc:search-files>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
suspend fun searchReport(webDavUrl: String, username: String, query: String, client: OkHttpClient): List<WebDavFile> {
|
||||
val requestBody = getSearchRequestBody(query)
|
||||
val request = Request.Builder()
|
||||
.url("${webDavUrl}files/$username")
|
||||
.method("REPORT", requestBody.toRequestBody())
|
||||
.build()
|
||||
return withContext(Dispatchers.IO) {
|
||||
val results = mutableListOf<WebDavFile>()
|
||||
try {
|
||||
val response = client.newCall(request).execute()
|
||||
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(response.body?.byteStream()
|
||||
?: return@withContext emptyList<WebDavFile>())
|
||||
val responses = document.getElementsByTagName("d:response")
|
||||
for (i in 0 until responses.length) {
|
||||
val res = responses.item(i) as? Element ?: continue
|
||||
val url = res.getElementsByTagName("d:href")
|
||||
.takeIf { it.length > 0 }?.item(0)
|
||||
?.textContent?.takeIf { it.isNotEmpty() } ?: continue
|
||||
val fileId = res.getElementsByTagName("oc:fileid")
|
||||
.takeIf { it.length > 0 }?.item(0)
|
||||
?.textContent?.toLongOrNull() ?: continue
|
||||
|
||||
val displayName = res.getElementsByTagName("d:displayname")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: url.trimEnd('/').substringAfterLast("/").decodeUrl("utf8")
|
||||
?: continue
|
||||
|
||||
val isDirectory = res.getElementsByTagName("d:resourcetype")
|
||||
.takeIf { it.length > 0 }
|
||||
?.item(0)?.childNodes?.length == 1
|
||||
val mimeType = res.getElementsByTagName("d:getcontenttype")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent?.takeIf { it.isNotEmpty() }
|
||||
?: if (isDirectory) "inode/directory" else "application/octet-stream"
|
||||
val size = res.getElementsByTagName("oc:size")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent?.toLongOrNull()
|
||||
?: 0L
|
||||
val owner = res.getElementsByTagName("oc:owner-display-name")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
|
||||
results += WebDavFile(
|
||||
name = displayName,
|
||||
id = fileId,
|
||||
isDirectory = isDirectory,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
owner = owner,
|
||||
url = url
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
return@withContext results
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.mm20.launcher2.webdav
|
||||
|
||||
data class WebDavFile(
|
||||
val name: String,
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val isDirectory: Boolean,
|
||||
val mimeType: String,
|
||||
val size: Long,
|
||||
val owner: String?
|
||||
)
|
||||
Reference in New Issue
Block a user