Reorganize and group modules

This commit is contained in:
MM20
2022-12-13 17:37:26 +01:00
parent bac24baad2
commit 3f8880a90a
995 changed files with 501 additions and 298 deletions
+2
View File
@@ -0,0 +1,2 @@
/build
*/**/g_services.json
+52
View File
@@ -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"))
}
View File
+23
View File
@@ -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
}
}
@@ -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"]}}
+1
View File
@@ -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)
}
+21
View File
@@ -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};
}
}
+2
View File
@@ -0,0 +1,2 @@
/build
*/**/msal_auth_config.json
+48
View File
@@ -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"))
}
View File
+23
View File
@@ -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?
)
@@ -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"
}
}
]
}
+1
View File
@@ -0,0 +1 @@
/build
+57
View File
@@ -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"))
}
View File
+21
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
/build
+59
View File
@@ -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"))
}
View File
+21
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
/build
+47
View File
@@ -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"))
}
View File
+21
View File
@@ -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
+4
View File
@@ -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?
)