ColorPickerDialogを少し整理

This commit is contained in:
tateisu 2024-03-17 19:05:30 +09:00
parent e57568a5b4
commit 15f54bdf00
64 changed files with 2122 additions and 1988 deletions

3
.gitignore vendored
View File

@ -96,7 +96,6 @@ fabric.properties
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
@ -221,3 +220,5 @@ keystore.properties
app/fcm/release/output-metadata.json
/_apk/
/.depCheck/

View File

@ -1,20 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8">
<bytecodeTargetLevel target="19">
<module name="apng" target="1.7" />
<module name="SubwayTooter.anko" target="17" />
<module name="SubwayTooter.apng_android" target="17" />
<module name="SubwayTooter.app" target="17" />
<module name="SubwayTooter.base" target="17" />
<module name="SubwayTooter.buildSrc" target="17" />
<module name="SubwayTooter.buildSrc.main" target="17" />
<module name="SubwayTooter.buildSrc.test" target="17" />
<module name="SubwayTooter.colorpicker" target="17" />
<module name="SubwayTooter.emoji" target="17" />
<module name="SubwayTooter.icon_material_symbols" target="17" />
<module name="SubwayTooter.buildSrc" target="20" />
<module name="SubwayTooter.buildSrc.main" target="20" />
<module name="SubwayTooter.buildSrc.test" target="20" />
<module name="SubwayTooter.main" target="17" />
<module name="SubwayTooter.sample_apng" target="17" />
<module name="SubwayTooter.test" target="17" />
</bytecodeTargetLevel>
</component>

View File

@ -16,7 +16,7 @@
</compositeBuild>
</compositeConfiguration>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="gradleJvm" value="#JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.23" />
<option name="version" value="1.9.22" />
</component>
</project>

View File

@ -45,12 +45,14 @@ android {
}
dependencies {
testImplementation("junit:junit:${Vers.junitVersion}")
androidTestImplementation("androidx.test.ext:junit:${Vers.androidxTestExtJunitVersion}")
androidTestImplementation("androidx.test.espresso:espresso-core:${Vers.androidxTestEspressoCoreVersion}")
implementation("androidx.appcompat:appcompat:${Vers.androidxAppcompat}")
implementation("androidx.core:core-ktx:${Vers.androidxCore}")
implementation("androidx.preference:preference-ktx:${Vers.androidxPreferenceKtx}")
implementation("com.google.android.material:material:${Vers.googleMaterial}")
implementation("androidx.appcompat:appcompat:${Vers.appcompatVersion}")
implementation("androidx.core:core-ktx:${Vers.coreKtxVersion}")
implementation("androidx.preference:preference-ktx:${Vers.preferenceKtxVersion}")
implementation("com.google.android.material:material:${Vers.materialVersion}")
testImplementation(kotlin("test"))
androidTestRuntimeOnly("androidx.test:runner:1.5.2")
androidTestImplementation("androidx.test:core:${Vers.androidxTestCore}")
androidTestImplementation("androidx.test.ext:junit:${Vers.androidxTestExtJunit}")
}

View File

@ -1,8 +1,7 @@
package jp.juggler.anko
import org.junit.Test
import org.junit.Assert.*
import kotlin.test.Test
import kotlin.test.assertEquals
/**
* Example local unit test, which will execute on the development machine (host).
@ -14,4 +13,4 @@ class ExampleUnitTest {
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
}

View File

@ -18,11 +18,11 @@ sub cmd($){
cmd "./gradlew --stop";
cmd "rm -rf .gradle/caches/build-cache-*";
cmd "./gradlew clean";
cmd "./dependencyJson.pl";
cmd "./gradlew assembleNoFcmRelease";
cmd "./gradlew assembleFcmRelease";
cmd "./gradlew --stop";
sub getBranch{
my $text = `git rev-parse --abbrev-ref HEAD`;
$text =~ s/\A\s+//;

View File

@ -27,7 +27,6 @@ compileTestKotlin.kotlinOptions{
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to arrayOf("*.jar"))))
testImplementation("junit:junit:${Vers.junitVersion}")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${Vers.kotlinVersion}")
// implementation(fileTree(mapOf("dir" to "libs", "include" to arrayOf("*.jar"))))
testImplementation(kotlin("test"))
}

View File

@ -2,11 +2,11 @@
package jp.juggler.apng
import org.junit.Assert.*
import org.junit.Test
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import kotlin.test.Test
import kotlin.test.assertEquals
@Suppress("LargeClass")
class TestApng {
@ -3090,9 +3090,8 @@ class TestApng {
)
}
private fun getResourceFile(path: String): File {
return File(this.javaClass.classLoader.getResource(path).path)
}
private fun getResourceFile(path: String): File =
File(javaClass.classLoader.getResource(path)!!.path)
@Test
fun testByteMod() {
@ -3124,10 +3123,10 @@ class TestApng {
}
override fun onAnimationInfo(
apng: Apng,
header: ApngImageHeader,
animationControl: ApngAnimationControl,
) {
apng: Apng,
header: ApngImageHeader,
animationControl: ApngAnimationControl,
) {
println("animationControl=$animationControl")
}
@ -3140,12 +3139,12 @@ class TestApng {
}
override fun onAnimationFrame(
apng: Apng,
frameControl: ApngFrameControl,
bitmap: ApngBitmap,
) {
apng: Apng,
frameControl: ApngFrameControl,
frameBitmap: ApngBitmap,
) {
println("onAnimationFrame frameControl=$frameControl")
println("onAnimationFrame w=${bitmap.width},h=${bitmap.height}")
println("onAnimationFrame w=${frameBitmap.width},h=${frameBitmap.height}")
}
}
)
@ -3154,7 +3153,6 @@ class TestApng {
@Test
fun test16bitPaeth() {
FileInputStream(getResourceFile("basi2c16.png")).use { inStream ->
ApngDecoder.parseStream(
BufferedInputStream(inStream),
@ -3172,10 +3170,10 @@ class TestApng {
}
override fun onAnimationInfo(
apng: Apng,
header: ApngImageHeader,
animationControl: ApngAnimationControl,
) {
apng: Apng,
header: ApngImageHeader,
animationControl: ApngAnimationControl,
) {
println("animationControl=$animationControl")
}
@ -3200,31 +3198,31 @@ class TestApng {
val dst_g = dstPos.green
val dst_b = dstPos.blue
assertEquals(
"xy=$x,$y color=${"0x%x".format(dstPos.color)} r",
src_r shr 8,
dst_r
dst_r,
"xy=$x,$y color=${"0x%x".format(dstPos.color)} r",
)
assertEquals(
"xy=$x,$y color=${"0x%x".format(dstPos.color)} g",
src_g shr 8,
dst_g
dst_g,
"xy=$x,$y color=${"0x%x".format(dstPos.color)} g",
)
assertEquals(
"xy=$x,$y color=${"0x%x".format(dstPos.color)} b",
src_b shr 8,
dst_b
dst_b,
"xy=$x,$y color=${"0x%x".format(dstPos.color)} b",
)
}
}
}
override fun onAnimationFrame(
apng: Apng,
frameControl: ApngFrameControl,
bitmap: ApngBitmap,
) {
apng: Apng,
frameControl: ApngFrameControl,
frameBitmap: ApngBitmap,
) {
println("onAnimationFrame frameControl=$frameControl")
println("onAnimationFrame w=${bitmap.width},h=${bitmap.height}")
println("onAnimationFrame w=${frameBitmap.width},h=${frameBitmap.height}")
}
}
)

View File

@ -62,10 +62,13 @@ repositories {
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:${Vers.desugarLibVersion}")
api(project(":apng"))
implementation(project(":base"))
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:${Vers.desugarLibVersion}")
implementation("com.github.bumptech.glide:glide:${Vers.glideVersion}")
implementation("com.github.zjupure:webpdecoder:${Vers.webpDecoderVersion}")
testImplementation("junit:junit:${Vers.junitVersion}")
// テストコードはない…
}

View File

@ -35,6 +35,10 @@ android {
buildFeatures {
viewBinding = true
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.10"
}
// exoPlayer 2.9.0 以降は Java 8 compiler support を要求する
@ -151,9 +155,7 @@ android {
}
dependencies {
// desugar_jdk_libs 2.0.0 は AGP 7.4.0-alpha10 以降を要求する
//noinspection GradleDependency
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:${Vers.desugarLibVersion}")
implementation(project(":base"))
@ -161,56 +163,96 @@ dependencies {
implementation(project(":emoji"))
implementation(project(":apng_android"))
implementation(project(":anko"))
implementation(fileTree(mapOf("dir" to "src/main/libs", "include" to arrayOf("*.aar"))))
implementation("androidx.activity:activity-compose:${Vers.androidxActivity}")
implementation("androidx.appcompat:appcompat:${Vers.androidxAppcompat}")
implementation("androidx.browser:browser:1.8.0")
implementation("androidx.compose.material3:material3:1.2.1")
implementation("androidx.compose.runtime:runtime-livedata:${Vers.androidxComposeRuntime}")
implementation("androidx.compose.ui:ui-tooling-preview:${Vers.androidxComposeUi}")
implementation("androidx.compose.ui:ui:${Vers.androidxComposeUi}")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.drawerlayout:drawerlayout:1.2.0")
implementation("androidx.emoji2:emoji2-bundled:${Vers.androidxEmoji2}")
implementation("androidx.emoji2:emoji2-views-helper:${Vers.androidxEmoji2}")
implementation("androidx.emoji2:emoji2-views:${Vers.androidxEmoji2}")
implementation("androidx.emoji2:emoji2:${Vers.androidxEmoji2}")
implementation("androidx.lifecycle:lifecycle-runtime-compose:${Vers.androidxLifecycle}")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:${Vers.androidxLifecycle}")
implementation("androidx.media3:media3-common:${Vers.androidxMedia3}")
implementation("androidx.media3:media3-datasource:${Vers.androidxMedia3}")
implementation("androidx.media3:media3-exoplayer:${Vers.androidxMedia3}")
implementation("androidx.media3:media3-session:${Vers.androidxMedia3}")
implementation("androidx.media3:media3-ui:${Vers.androidxMedia3}")
implementation("androidx.recyclerview:recyclerview:${Vers.androidxRecyclerView}")
implementation("androidx.work:work-runtime-ktx:${Vers.androidxWork}")
implementation("androidx.work:work-runtime:${Vers.androidxWork}")
implementation("com.caverock:androidsvg-aar:1.4")
implementation("com.github.UnifiedPush:android-connector:2.1.1")
implementation("com.github.alexzhirkevich:custom-qr-generator:1.6.2")
implementation("com.github.bumptech.glide:glide:${Vers.glideVersion}")
implementation("com.github.omadahealth:swipy:1.2.3@aar")
implementation("com.github.penfeizhou.android.animation:apng:${Vers.apng4AndroidVersion}")
implementation("com.github.woxthebox:draglistview:1.7.3")
implementation("com.github.zjupure:webpdecoder:${Vers.webpDecoderVersion}")
implementation("com.google.android.flexbox:flexbox:${Vers.googleFlexbox}")
implementation("com.google.android.material:material:${Vers.googleMaterial}")
implementation("com.squareup.okhttp3:okhttp-urlconnection:${Vers.okhttpVersion}")
implementation("com.squareup.okhttp3:okhttp:${Vers.okhttpVersion}")
implementation("com.squareup.okhttp3:okhttp:${Vers.okhttpVersion}")
implementation("io.insert-koin:koin-android-compat:${Vers.koinVersion}")
implementation("io.insert-koin:koin-android:${Vers.koinVersion}")
implementation("io.insert-koin:koin-androidx-workmanager:${Vers.koinVersion}")
implementation("org.conscrypt:conscrypt-android:${Vers.conscryptVersion}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${Vers.kotlinxSerializationLibVersion}")
implementation("ru.gildor.coroutines:kotlin-coroutines-okhttp:${Vers.gildorkotlinCoroutinesOkhttp}")
////////////
implementation("com.github.bumptech.glide:okhttp3-integration:${Vers.glideVersion}") {
exclude("com.squareup.okhttp3", "okhttp")
}
"fcmImplementation"("com.google.firebase:firebase-messaging:23.4.1")
"fcmImplementation"("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:${Vers.kotlinxCoroutinesVersion}")
implementation("androidx.core:core-splashscreen:1.0.1")
// implementation "org.conscrypt:conscrypt-android:$conscryptVersion"
api("org.conscrypt:conscrypt-android:${Vers.conscryptVersion}")
implementation("com.github.UnifiedPush:android-connector:2.1.1")
implementation("jp.wasabeef:glide-transformations:4.3.0")
// implementation("com.github.androidmads:QRGenerator:1.0.1")
implementation("com.github.alexzhirkevich:custom-qr-generator:1.6.2")
val apng4AndroidVersion = "2.25.0"
implementation("com.github.penfeizhou.android.animation:apng:$apng4AndroidVersion")
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:${Vers.detektVersion}")
ksp("com.github.bumptech.glide:ksp:${Vers.glideVersion}")
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:${Vers.detektVersion}")
debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.3")
debugImplementation("androidx.compose.ui:ui-tooling:1.6.3")
testImplementation(project(":base"))
androidTestImplementation(project(":base"))
// =================================================
// UnitTest
testImplementation(kotlin("test"))
testImplementation("androidx.arch.core:core-testing:${Vers.androidxArchCoreTesting}")
testImplementation("junit:junit:${Vers.junitVersion}")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Vers.kotlinxCoroutinesVersion}")
androidTestApi("androidx.test.espresso:espresso-core:${Vers.androidxTestEspressoCoreVersion}")
androidTestApi("androidx.test.ext:junit-ktx:1.1.5")
androidTestApi("androidx.test.ext:junit:${Vers.androidxTestExtJunitVersion}")
androidTestApi("androidx.test.ext:truth:1.5.0")
androidTestApi("androidx.test:core-ktx:${Vers.testKtxVersion}")
androidTestApi("androidx.test:core:${Vers.androidxTestVersion}")
androidTestApi("androidx.test:runner:1.5.2")
androidTestApi("org.jetbrains.kotlin:kotlin-test:${Vers.kotlinTestVersion}")
androidTestApi("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Vers.kotlinxCoroutinesVersion}")
testApi("androidx.arch.core:core-testing:${Vers.archVersion}")
testApi("junit:junit:${Vers.junitVersion}")
testApi("org.jetbrains.kotlin:kotlin-test:${Vers.kotlinTestVersion}")
testApi("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Vers.kotlinxCoroutinesVersion}")
// testImplementation("com.squareup.okhttp3:mockwebserver:${Vers.okhttpVersion}") {
// exclude("com.squareup.okio", "okio")
// exclude("com.squareup.okhttp3", "okhttp")
// exclude("org.jetbrains.kotlin", "kotlin-stdlib-common")
// exclude("org.jetbrains.kotlin", "kotlin-stdlib")
// exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
// }
// To use android test orchestrator
// ==============================================
// androidTest
androidTestRuntimeOnly("androidx.test:runner:1.5.2")
androidTestUtil("androidx.test:orchestrator:1.4.2")
testApi("com.squareup.okhttp3:mockwebserver:${Vers.okhttpVersion}") {
exclude("com.squareup.okio", "okio")
exclude("com.squareup.okhttp3", "okhttp")
exclude("org.jetbrains.kotlin", "kotlin-stdlib-common")
exclude("org.jetbrains.kotlin", "kotlin-stdlib")
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
}
androidTestApi("com.squareup.okhttp3:mockwebserver:${Vers.okhttpVersion}") {
androidTestImplementation("androidx.test.espresso:espresso-core:${Vers.androidxTestEspressoCore}")
androidTestImplementation("androidx.test.ext:junit-ktx:1.1.5")
androidTestImplementation("androidx.test.ext:junit:${Vers.androidxTestExtJunit}")
androidTestImplementation("androidx.test.ext:truth:1.5.0")
androidTestImplementation("androidx.test:core-ktx:${Vers.androidxTestCoreKtx}")
androidTestImplementation("androidx.test:core:${Vers.androidxTestCore}")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.6.3")
androidTestImplementation("com.squareup.okhttp3:mockwebserver:${Vers.okhttpVersion}") {
exclude("com.squareup.okio", "okio")
exclude("com.squareup.okhttp3", "okhttp")
exclude("org.jetbrains.kotlin", "kotlin-stdlib-common")

View File

@ -2,7 +2,7 @@ package jp.juggler.subwaytooter
import android.graphics.Color
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.jrummyapps.android.colorpicker.parseColorString
import com.jrummyapps.android.colorpicker.parseColor
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@ -12,8 +12,8 @@ class TestColorString {
@Test
fun testColorString() {
fun a(s: String, expect: Int) {
assertEquals(s, expect, parseColorString(s))
assertEquals("#$s", expect, parseColorString("#$s"))
assertEquals(s, expect, s.parseColor())
assertEquals("#$s", expect, "#$s".parseColor())
}
a("", Color.BLACK)
a("8", Color.BLACK or 0x88_88_88)

File diff suppressed because one or more lines are too long

View File

@ -252,7 +252,7 @@
android:label="@string/app_about" />
<activity
android:name=".ActOSSLicense"
android:name=".ui.ossLicense.ActOSSLicense"
android:exported="false"
android:label="@string/oss_license" />

View File

@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.os.Handler
@ -22,8 +21,9 @@ import android.widget.Spinner
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.jrummyapps.android.colorpicker.ColorPickerDialog
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
import androidx.lifecycle.lifecycleScope
import com.jrummyapps.android.colorpicker.ColorPickerDialogType
import com.jrummyapps.android.colorpicker.dialogColorPicker
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
@ -77,6 +77,8 @@ import jp.juggler.util.ui.isEnabledAlpha
import jp.juggler.util.ui.isOk
import jp.juggler.util.ui.scan
import jp.juggler.util.ui.vg
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import okhttp3.MediaType
@ -94,7 +96,6 @@ import kotlin.math.max
class ActAccountSetting : AppCompatActivity(),
View.OnClickListener,
ColorPickerDialogListener,
CompoundButton.OnCheckedChangeListener,
AdapterView.OnItemSelectedListener {
companion object {
@ -722,13 +723,19 @@ class ActAccountSetting : AppCompatActivity(),
// )
R.id.btnNotificationAccentColorEdit -> {
ColorPickerDialog.newBuilder().apply {
setDialogType(ColorPickerDialog.TYPE_CUSTOM)
setAllowPresets(true)
setShowAlphaSlider(false)
setDialogId(COLOR_DIALOG_NOTIFICATION_ACCENT_COLOR)
account.notificationAccentColor.notZero()?.let { setColor(it) }
}.show(this)
lifecycleScope.launch {
try {
account.notificationAccentColor = dialogColorPicker(
colorInitial = account.notificationAccentColor.notZero(),
alphaEnabled = false,
)
showNotificationColor()
saveUIToData()
} catch (ex: Throwable) {
if (ex is CancellationException) return@launch
log.e(ex, "openColorPicker failed.")
}
}
}
R.id.btnNotificationAccentColorReset -> {
@ -1518,21 +1525,6 @@ class ActAccountSetting : AppCompatActivity(),
return rv
}
override fun onDialogDismissed(dialogId: Int) {
}
override fun onColorSelected(dialogId: Int, newColor: Int) {
when (dialogId) {
COLOR_DIALOG_NOTIFICATION_ACCENT_COLOR -> {
account.notificationAccentColor = newColor or Color.BLACK
showNotificationColor()
saveUIToData()
}
else -> Unit
}
}
private fun showNotificationColor() {
views.vNotificationAccentColorColor.backgroundColor =
account.notificationAccentColor.notZero()

View File

@ -27,17 +27,17 @@ import android.widget.FrameLayout
import android.widget.Spinner
import android.widget.TextView
import android.widget.TextView.OnEditorActionListener
import androidx.annotation.ColorInt
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.jrummyapps.android.colorpicker.ColorPickerDialog
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
import com.jrummyapps.android.colorpicker.ColorPickerDialogType
import com.jrummyapps.android.colorpicker.dialogColorPicker
import jp.juggler.subwaytooter.appsetting.AppDataExporter
import jp.juggler.subwaytooter.appsetting.AppSettingItem
import jp.juggler.subwaytooter.appsetting.SettingType
@ -63,8 +63,8 @@ import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchProgress
import jp.juggler.util.data.cast
import jp.juggler.util.data.defaultLocale
import jp.juggler.util.data.checkMimeTypeAndGrant
import jp.juggler.util.data.defaultLocale
import jp.juggler.util.data.intentOpenDocument
import jp.juggler.util.data.notEmpty
import jp.juggler.util.data.notZero
@ -81,6 +81,8 @@ import jp.juggler.util.ui.isEnabledAlpha
import jp.juggler.util.ui.isNotOk
import jp.juggler.util.ui.launch
import jp.juggler.util.ui.vg
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
@ -94,7 +96,7 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import kotlin.math.abs
class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnClickListener {
class ActAppSetting : AppCompatActivity(), View.OnClickListener {
companion object {
@ -393,22 +395,6 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
private fun dip(dp: Int): Int = dip(dp.toFloat())
override fun onDialogDismissed(dialogId: Int) {
}
override fun onColorSelected(dialogId: Int, @ColorInt newColor: Int) {
val colorTarget = this.colorTarget ?: return
val ip: IntPref = colorTarget.pref.cast() ?: error("$colorTarget has no in pref")
val c = when (colorTarget.type) {
SettingType.ColorAlpha -> newColor.notZero() ?: 1
else -> newColor or Color.BLACK
}
ip.value = c
val vh = findItemViewHolder(colorTarget)
vh?.showColor()
colorTarget.changed(this)
}
private val settingHolderList =
ConcurrentHashMap<Int, VhSettingItem>()
@ -655,15 +641,25 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
views.btnEdit.isEnabledAlpha = item.enabled
views.btnReset.isEnabledAlpha = item.enabled
views.btnEdit.setOnClickListener {
actAppSetting.colorTarget = item
val color = ip.value
val builder = ColorPickerDialog.newBuilder()
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
.setAllowPresets(true)
.setShowAlphaSlider(item.type == SettingType.ColorAlpha)
.setDialogId(COLOR_DIALOG_ID)
if (color != 0) builder.setColor(color)
builder.show(actAppSetting)
actAppSetting.lifecycleScope.launch {
try {
actAppSetting.colorTarget = item
val newColor = actAppSetting.dialogColorPicker(
colorInitial = ip.value.notZero(),
alphaEnabled = item.type == SettingType.ColorAlpha,
)
val c = when (item.type) {
SettingType.ColorAlpha -> newColor.notZero() ?: 1
else -> newColor or Color.BLACK
}
ip.value = c
item.changed(actAppSetting)
actAppSetting.findItemViewHolder(item)?.showColor()
} catch (ex: Throwable) {
if (ex is CancellationException) return@launch
log.e(ex, "")
}
}
}
views.btnReset.setOnClickListener {
ip.removeValue()

View File

@ -12,31 +12,46 @@ import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.ImageView
import android.widget.SeekBar
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import com.jrummyapps.android.colorpicker.ColorPickerDialog
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
import com.jrummyapps.android.colorpicker.dialogColorPicker
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.column.*
import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.column.getAcctColor
import jp.juggler.subwaytooter.column.getBackgroundImageDir
import jp.juggler.subwaytooter.column.getColumnName
import jp.juggler.subwaytooter.column.getContentColor
import jp.juggler.subwaytooter.column.getHeaderBackgroundColor
import jp.juggler.subwaytooter.column.getHeaderNameColor
import jp.juggler.subwaytooter.column.getIconId
import jp.juggler.subwaytooter.column.setHeaderBackground
import jp.juggler.subwaytooter.databinding.ActColumnCustomizeBinding
import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.*
import jp.juggler.util.data.checkMimeTypeAndGrant
import jp.juggler.util.data.defaultLocale
import jp.juggler.util.data.intentGetContent
import jp.juggler.util.data.mayUri
import jp.juggler.util.data.notZero
import jp.juggler.util.int
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption
import jp.juggler.util.media.createResizedBitmap
import jp.juggler.util.ui.*
import jp.juggler.util.ui.ActivityResultHandler
import jp.juggler.util.ui.hideKeyboard
import jp.juggler.util.ui.isNotOk
import jp.juggler.util.ui.setNavigationBack
import jp.juggler.util.ui.vg
import org.jetbrains.anko.textColor
import java.io.File
import java.io.FileOutputStream
import java.text.NumberFormat
import kotlin.math.max
class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPickerDialogListener {
class ActColumnCustomize : AppCompatActivity(), View.OnClickListener {
companion object {
@ -44,12 +59,6 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
internal const val EXTRA_COLUMN_INDEX = "column_index"
internal const val COLOR_DIALOG_ID_HEADER_BACKGROUND = 1
internal const val COLOR_DIALOG_ID_HEADER_FOREGROUND = 2
internal const val COLOR_DIALOG_ID_COLUMN_BACKGROUND = 3
internal const val COLOR_DIALOG_ID_ACCT_TEXT = 4
internal const val COLOR_DIALOG_ID_CONTENT_TEXT = 5
internal const val PROGRESS_MAX = 65536
fun createIntent(activity: ActMain, idx: Int) =
@ -107,19 +116,13 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
}
override fun onClick(v: View) {
val builder: ColorPickerDialog.Builder
when (v.id) {
R.id.btnHeaderBackgroundEdit -> {
ColorPickerDialog.newBuilder()
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
.setAllowPresets(true)
.setShowAlphaSlider(false)
.setDialogId(COLOR_DIALOG_ID_HEADER_BACKGROUND)
.setColor(column.getHeaderBackgroundColor())
.show(this)
R.id.btnHeaderBackgroundEdit -> launchAndShowError {
column.headerBgColor = Color.BLACK or dialogColorPicker(
colorInitial = column.getHeaderBackgroundColor(),
alphaEnabled = false,
)
}
R.id.btnHeaderBackgroundReset -> {
@ -127,14 +130,11 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
show()
}
R.id.btnHeaderTextEdit -> {
ColorPickerDialog.newBuilder()
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
.setAllowPresets(true)
.setShowAlphaSlider(false)
.setDialogId(COLOR_DIALOG_ID_HEADER_FOREGROUND)
.setColor(column.getHeaderNameColor())
.show(this)
R.id.btnHeaderTextEdit -> launchAndShowError {
column.headerFgColor = Color.BLACK or dialogColorPicker(
colorInitial = column.getHeaderNameColor(),
alphaEnabled = false,
)
}
R.id.btnHeaderTextReset -> {
@ -142,14 +142,11 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
show()
}
R.id.btnColumnBackgroundColor -> {
builder = ColorPickerDialog.newBuilder()
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
.setAllowPresets(true)
.setShowAlphaSlider(false)
.setDialogId(COLOR_DIALOG_ID_COLUMN_BACKGROUND)
if (column.columnBgColor != 0) builder.setColor(column.columnBgColor)
builder.show(this)
R.id.btnColumnBackgroundColor -> launchAndShowError {
column.columnBgColor = Color.BLACK or dialogColorPicker(
colorInitial = column.columnBgColor.notZero(),
alphaEnabled = false,
)
}
R.id.btnColumnBackgroundColorReset -> {
@ -157,14 +154,11 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
show()
}
R.id.btnAcctColor -> {
ColorPickerDialog.newBuilder()
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
.setAllowPresets(true)
.setShowAlphaSlider(true)
.setDialogId(COLOR_DIALOG_ID_ACCT_TEXT)
.setColor(column.getAcctColor())
.show(this)
R.id.btnAcctColor -> launchAndShowError {
column.acctColor = dialogColorPicker(
colorInitial = column.getAcctColor(),
alphaEnabled = true,
).notZero() ?: 1
}
R.id.btnAcctColorReset -> {
@ -172,14 +166,11 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
show()
}
R.id.btnContentColor -> {
ColorPickerDialog.newBuilder()
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
.setAllowPresets(true)
.setShowAlphaSlider(true)
.setDialogId(COLOR_DIALOG_ID_CONTENT_TEXT)
.setColor(column.getContentColor())
.show(this)
R.id.btnContentColor -> launchAndShowError {
column.contentColor = dialogColorPicker(
colorInitial = column.getContentColor(),
alphaEnabled = true,
).notZero() ?: 1
}
R.id.btnContentColorReset -> {
@ -203,28 +194,6 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
}
}
override fun onColorSelected(dialogId: Int, @ColorInt newColor: Int) {
when (dialogId) {
COLOR_DIALOG_ID_HEADER_BACKGROUND ->
column.headerBgColor = Color.BLACK or newColor
COLOR_DIALOG_ID_HEADER_FOREGROUND ->
column.headerFgColor = Color.BLACK or newColor
COLOR_DIALOG_ID_COLUMN_BACKGROUND ->
column.columnBgColor = Color.BLACK or newColor
COLOR_DIALOG_ID_ACCT_TEXT ->
column.acctColor = newColor.notZero() ?: 1
COLOR_DIALOG_ID_CONTENT_TEXT ->
column.contentColor = newColor.notZero() ?: 1
}
show()
}
override fun onDialogDismissed(dialogId: Int) {}
private fun updateBackground(uriArg: Uri) {
launchMain {
var resultUri: String? = null

View File

@ -8,8 +8,7 @@ import android.media.RingtoneManager
import android.os.Bundle
import android.widget.CompoundButton
import androidx.appcompat.app.AppCompatActivity
import com.jrummyapps.android.colorpicker.ColorPickerDialog
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
import com.jrummyapps.android.colorpicker.dialogColorPicker
import jp.juggler.subwaytooter.databinding.ActHighlightEditBinding
import jp.juggler.subwaytooter.table.HighlightWord
import jp.juggler.subwaytooter.table.daoHighlightWord
@ -23,12 +22,15 @@ import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
import jp.juggler.util.long
import jp.juggler.util.string
import jp.juggler.util.ui.*
import jp.juggler.util.ui.ActivityResultHandler
import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.decodeRingtonePickerResult
import jp.juggler.util.ui.isEnabledAlpha
import jp.juggler.util.ui.setNavigationBack
import org.jetbrains.anko.textColor
class ActHighlightWordEdit
: AppCompatActivity(),
ColorPickerDialogListener,
CompoundButton.OnCheckedChangeListener {
companion object {
@ -141,20 +143,26 @@ class ActHighlightWordEdit
views.btnDiscard.setOnClickListener { finish() }
views.btnSave.setOnClickListener { save() }
views.btnTextColorEdit.setOnClickListener {
openColorPicker(
COLOR_DIALOG_ID_TEXT,
item.color_fg
)
launchAndShowError {
item.color_fg = Color.BLACK or dialogColorPicker(
colorInitial = item.color_fg.notZero(),
alphaEnabled = false,
)
showColor()
}
}
views.btnTextColorReset.setOnClickListener {
item.color_fg = 0
showColor()
}
views.btnBackgroundColorEdit.setOnClickListener {
openColorPicker(
COLOR_DIALOG_ID_BACKGROUND,
item.color_bg
)
launchAndShowError {
item.color_bg = dialogColorPicker(
colorInitial = item.color_bg.notZero(),
alphaEnabled = true,
).notZero() ?: 0x01000000
showColor()
}
}
views.btnBackgroundColorReset.setOnClickListener {
item.color_bg = 0
@ -180,28 +188,6 @@ class ActHighlightWordEdit
showSound()
}
private fun openColorPicker(id: Int, initialColor: Int) {
val builder = ColorPickerDialog.newBuilder()
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
.setAllowPresets(true)
.setShowAlphaSlider(id == COLOR_DIALOG_ID_BACKGROUND)
.setDialogId(id)
if (initialColor != 0) builder.setColor(initialColor)
builder.show(this)
}
override fun onDialogDismissed(dialogId: Int) {}
override fun onColorSelected(dialogId: Int, newColor: Int) {
when (dialogId) {
COLOR_DIALOG_ID_TEXT -> item.color_fg = newColor or Color.BLACK
COLOR_DIALOG_ID_BACKGROUND -> item.color_bg = newColor.notZero() ?: 0x01000000
}
showColor()
}
//////////////////////////////////////////////////////////////////
private fun showSound() {

View File

@ -2,15 +2,14 @@ package jp.juggler.subwaytooter
import android.app.Activity
import android.content.Intent
import android.graphics.Color
import android.media.RingtoneManager
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import com.jrummyapps.android.colorpicker.ColorPickerDialog
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
import com.jrummyapps.android.colorpicker.dialogColorPicker
import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.databinding.ActNicknameBinding
import jp.juggler.subwaytooter.table.AcctColor
@ -23,11 +22,17 @@ import jp.juggler.util.data.notEmpty
import jp.juggler.util.data.notZero
import jp.juggler.util.log.LogCategory
import jp.juggler.util.string
import jp.juggler.util.ui.*
import jp.juggler.util.ui.ActivityResultHandler
import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.decodeRingtonePickerResult
import jp.juggler.util.ui.hideKeyboard
import jp.juggler.util.ui.isEnabledAlpha
import jp.juggler.util.ui.setNavigationBack
import jp.juggler.util.ui.vg
import org.jetbrains.anko.backgroundColor
import org.jetbrains.anko.textColor
class ActNickname : AppCompatActivity(), View.OnClickListener, ColorPickerDialogListener {
class ActNickname : AppCompatActivity(), View.OnClickListener {
companion object {
private val log = LogCategory("ActNickname")
@ -170,17 +175,14 @@ class ActNickname : AppCompatActivity(), View.OnClickListener, ColorPickerDialog
}
override fun onClick(v: View) {
val builder: ColorPickerDialog.Builder
when (v.id) {
R.id.btnTextColorEdit -> {
R.id.btnTextColorEdit -> launchAndShowError {
views.etNickname.hideKeyboard()
builder = ColorPickerDialog.newBuilder()
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
.setAllowPresets(true)
.setShowAlphaSlider(false)
.setDialogId(1)
if (colorFg != 0) builder.setColor(colorFg)
builder.show(this)
colorFg = Color.BLACK or dialogColorPicker(
colorInitial = colorFg.notZero(),
alphaEnabled = false
)
show()
}
R.id.btnTextColorReset -> {
@ -188,15 +190,13 @@ class ActNickname : AppCompatActivity(), View.OnClickListener, ColorPickerDialog
show()
}
R.id.btnBackgroundColorEdit -> {
R.id.btnBackgroundColorEdit -> launchAndShowError {
views.etNickname.hideKeyboard()
builder = ColorPickerDialog.newBuilder()
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
.setAllowPresets(true)
.setShowAlphaSlider(false)
.setDialogId(2)
if (colorBg != 0) builder.setColor(colorBg)
builder.show(this)
colorBg = Color.BLACK or dialogColorPicker(
colorInitial = colorBg.notZero(),
alphaEnabled = false,
)
show()
}
R.id.btnBackgroundColorReset -> {
@ -221,16 +221,6 @@ class ActNickname : AppCompatActivity(), View.OnClickListener, ColorPickerDialog
}
}
override fun onColorSelected(dialogId: Int, @ColorInt newColor: Int) {
when (dialogId) {
1 -> colorFg = -0x1000000 or newColor
2 -> colorBg = -0x1000000 or newColor
}
show()
}
override fun onDialogDismissed(dialogId: Int) {}
private fun openNotificationSoundPicker() {
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION)

View File

@ -1,35 +0,0 @@
package jp.juggler.subwaytooter
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.databinding.ActOssLicenseBinding
import jp.juggler.util.data.decodeUTF8
import jp.juggler.util.data.loadRawResource
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.setNavigationBack
class ActOSSLicense : AppCompatActivity() {
companion object {
private val log = LogCategory("ActOSSLicense")
}
private val views by lazy {
ActOssLicenseBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
App1.setActivityTheme(this)
setContentView(views.root)
setSupportActionBar(views.toolbar)
setNavigationBack(views.toolbar)
fixHorizontalMargin(views.svContent)
try {
views.tvText.text = loadRawResource(R.raw.oss_license).decodeUTF8()
} catch (ex: Throwable) {
log.e(ex, "can't show license text.")
}
}
}

View File

@ -1,6 +1,7 @@
package jp.juggler.subwaytooter
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.res.Configuration
@ -405,7 +406,7 @@ class App1 : Application() {
}
fun setActivityTheme(
activity: AppCompatActivity,
activity: Activity,
forceDark: Boolean = false,
) {
prepare(activity.applicationContext, "setActivityTheme")

View File

@ -1,5 +1,6 @@
package jp.juggler.subwaytooter
import android.app.Activity
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Configuration
@ -469,7 +470,7 @@ fun ViewGroup.generateLayoutParamsEx(): ViewGroup.LayoutParams? =
null
}
fun AppCompatActivity.setStatusBarColor(forceDark: Boolean = false) {
fun Activity.setStatusBarColor(forceDark: Boolean = false) {
window?.apply {
if (Build.VERSION.SDK_INT < 30) {
@Suppress("DEPRECATION")

View File

@ -28,7 +28,7 @@ import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.ActMutedApp
import jp.juggler.subwaytooter.ActMutedPseudoAccount
import jp.juggler.subwaytooter.ActMutedWord
import jp.juggler.subwaytooter.ActOSSLicense
import jp.juggler.subwaytooter.ui.ossLicense.ActOSSLicense
import jp.juggler.subwaytooter.ActPushMessageList
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R

View File

@ -0,0 +1,233 @@
package jp.juggler.subwaytooter.ui.ossLicense
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.databinding.ActOssLicenseBinding
import jp.juggler.subwaytooter.util.openBrowser
import jp.juggler.subwaytooter.util.provideViewModel
import jp.juggler.subwaytooter.util.toAnnotatedString
import jp.juggler.util.data.mayUri
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class ActOSSLicense : ComponentActivity() {
companion object {
private val log = LogCategory("ActOSSLicense")
}
private val views by lazy {
ActOssLicenseBinding.inflate(layoutInflater)
}
private val viewModel by lazy {
provideViewModel(this) {
ActOSSLicenseViewModel(application)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
App1.setActivityTheme(this)
setContent {
Screen(
librariesFlow = viewModel.libraries,
isProgressShownFlow = viewModel.isProgressShown,
)
}
try {
viewModel.load()
} catch (ex: Throwable) {
log.e(ex, "dependency in fo loading failed.")
}
}
@Preview
@Composable
fun DefaultPreview() {
Screen(
isProgressShownFlow = MutableStateFlow(false),
librariesFlow = MutableStateFlow(
listOf(
LibText(
nameBig = "nameBig1".toAnnotatedString(),
nameSmall = "nameSmall".toAnnotatedString(),
desc = "desc".toAnnotatedString(),
),
LibText(
nameBig = "nameBig2".toAnnotatedString(),
nameSmall = "nameSmall".toAnnotatedString(),
desc = "desc".toAnnotatedString(),
),
LibText(
nameBig = "nameBig3".toAnnotatedString(),
nameSmall = "nameSmall".toAnnotatedString(),
desc = "desc".toAnnotatedString(),
),
)
),
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Screen(
librariesFlow: Flow<List<LibText>>,
isProgressShownFlow: Flow<Boolean>,
) {
val isProgressShown = isProgressShownFlow.collectAsState(false)
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
TopAppBar(
title = {
Text(stringResource(R.string.oss_license))
},
navigationIcon = {
IconButton(
onClick = { finish() }
) {
Icon(
// imageVector = AutoMirrored.Outlined.ArrowBack,
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.close)
)
}
},
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.topAppBarColors(
// containerColor = : Color = Color.Unspecified,
// scrolledContainerColor: Color = Color.Unspecified,
// navigationIconContentColor: Color = Color.Unspecified,
// titleContentColor: Color = Color.Unspecified,
// actionIconContentColor: Color = Color.Unspecified,
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary,
),
)
},
) { innerPadding ->
when (isProgressShown.value) {
true -> Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
modifier = Modifier.width(64.dp),
color = MaterialTheme.colorScheme.secondary,
trackColor = MaterialTheme.colorScheme.surfaceVariant,
)
}
else -> ScrollContent(innerPadding, librariesFlow)
}
}
}
@Composable
fun ScrollContent(
innerPadding: PaddingValues,
librariesFlow: Flow<List<LibText>>,
) {
val libraries = librariesFlow.collectAsState(emptyList())
LazyColumn(
modifier = Modifier.padding(innerPadding),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
items(libraries.value) { LibTextCard(it) }
}
}
@Composable
fun LibTextCard(src: LibText) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp, horizontal = 12.dp)
) {
src.nameBig.notEmpty()?.let {
ClickableText(
text = it,
style = MaterialTheme.typography.headlineMedium,
) { offset ->
it.getStringAnnotations(
tag = "URL",
start = offset,
end = offset,
).firstOrNull()?.let { a ->
openBrowser(a.item.mayUri())
}
}
}
src.nameSmall.notEmpty()?.let {
ClickableText(
text = it,
style = MaterialTheme.typography.bodySmall,
) { offset ->
it.getStringAnnotations(
tag = "URL",
start = offset,
end = offset,
).firstOrNull()?.let { a ->
openBrowser(a.item.mayUri())
}
}
}
Spacer(modifier = Modifier.height(8.dp))
src.desc.notEmpty()?.let {
ClickableText(
text = it,
style = MaterialTheme.typography.bodyMedium,
) { offset ->
it.getStringAnnotations(
tag = "URL",
start = offset,
end = offset,
).firstOrNull()?.let { a ->
openBrowser(a.item.mayUri())
}
}
}
}
}
}

View File

@ -3,14 +3,10 @@ package jp.juggler.subwaytooter.util
import jp.juggler.util.log.LogCategory
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.MediaType
import okhttp3.Response
import okhttp3.ResponseBody
import okio.*
import java.io.IOException
import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.charset.Charset
import kotlin.math.max
class ProgressResponseBody private constructor(
@ -18,24 +14,24 @@ class ProgressResponseBody private constructor(
) : ResponseBody() {
companion object {
internal val log = LogCategory("ProgressResponseBody")
fun makeInterceptor(): Interceptor = Interceptor { chain ->
// ProgressResponseBody を間に挟むインタセプタを作成する
fun makeInterceptor() = Interceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body))
.build()
}
// 進捗コールバックつきでバイト列を読む
@Throws(IOException::class)
fun bytes(
response: Response,
callback: suspend (bytesRead: Long, bytesTotal: Long) -> Unit,
): ByteArray {
return bytes(response.body, callback)
}
) = bytes(response.body, callback)
// 進捗コールバックつきでバイト列を読む
@Suppress("MemberVisibilityCanPrivate")
@Throws(IOException::class)
private fun bytes(
@ -49,204 +45,25 @@ class ProgressResponseBody private constructor(
private var callback: suspend (bytesRead: Long, bytesTotal: Long) -> Unit = { _, _ -> }
/*
RequestBody.bytes() is defined as final, We can't override it.
Make WrappedBufferedSource to capture BufferedSource.readByteArray().
*/
private val wrappedSource: BufferedSource by lazy {
val originalSource = originalBody.source()
try {
// if it is RealBufferedSource, I can access to source public field via reflection.
val fieldSource = originalSource.javaClass.getField("source")
// If there is the method, create the wrapper.
object : ForwardingBufferedSource(originalSource) {
@Throws(IOException::class)
override fun readByteArray(): ByteArray {
/*
RealBufferedSource.readByteArray() does:
- buffer.writeAll(source);
- return buffer.readByteArray(buffer.size());
We do same things using Reflection, with progress.
*/
try {
val contentLength = originalBody.contentLength()
val buffer = originalSource.buffer
val source = fieldSource.get(originalSource) as Source?
?: throw IllegalArgumentException("source == null")
// same thing of Buffer.writeAll(), with counting.
var nRead: Long = 0
runBlocking { callback(0, max(contentLength, 1)) }
while (true) {
val delta = source.read(buffer, 8192)
if (delta == -1L) break
nRead += delta
if (nRead > 0) {
runBlocking { callback(nRead, max(contentLength, nRead)) }
}
}
// EOS時の進捗
runBlocking { callback(nRead, max(contentLength, nRead)) }
return buffer.readByteArray()
} catch (ex: Throwable) {
log.e(ex, "readByteArray() failed.")
return originalSource.readByteArray()
}
private val wrappedSource by lazy {
object : ForwardingSource(originalBody.source()) {
var totalBytesRead = 0L
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
// read() returns the number of bytes read, or -1 if this source is exhausted.
totalBytesRead += max(0, bytesRead)
runBlocking {
callback.invoke(
totalBytesRead,
originalBody.contentLength(),
)
}
return bytesRead
}
} catch (ex: Throwable) {
log.e(ex, "can't access to RealBufferedSource#source field.")
originalSource
}
}.buffer()
}
/*
then you can read response body's bytes() with progress callback.
example:
byte[] data = ProgressResponseBody.bytes( response, new ProgressResponseBody.Callback() {
@Override public void progressBytes( long bytesRead, long bytesTotal ){
publishApiProgressRatio( (int) bytesRead, (int) bytesTotal );
}
} );
*/
override fun contentType(): MediaType? {
return originalBody.contentType()
}
override fun contentLength(): Long {
return originalBody.contentLength()
}
override fun source(): BufferedSource = wrappedSource
// To avoid double buffering, We have to make ForwardingBufferedSource.
internal open class ForwardingBufferedSource(
private val originalSource: BufferedSource,
) : BufferedSource {
override val buffer: Buffer
get() = originalSource.buffer
@Deprecated("use val buffer.", replaceWith = ReplaceWith("buffer"))
@Suppress("OverridingDeprecatedMember")
override fun buffer() = buffer
override fun peek(): BufferedSource = originalSource.peek()
override fun read(dst: ByteBuffer?) = originalSource.read(dst)
override fun isOpen() = originalSource.isOpen
override fun exhausted() = originalSource.exhausted()
override fun require(byteCount: Long) = originalSource.require(byteCount)
override fun request(byteCount: Long) = originalSource.request(byteCount)
override fun readByte() = originalSource.readByte()
override fun readShort() = originalSource.readShort()
override fun readShortLe() = originalSource.readShortLe()
override fun readInt() = originalSource.readInt()
override fun readIntLe() = originalSource.readIntLe()
override fun readLong() = originalSource.readLong()
override fun readLongLe() = originalSource.readLongLe()
override fun readDecimalLong() = originalSource.readDecimalLong()
override fun readHexadecimalUnsignedLong() = originalSource.readHexadecimalUnsignedLong()
override fun skip(byteCount: Long) = originalSource.skip(byteCount)
override fun readByteString(): ByteString = originalSource.readByteString()
override fun readByteString(byteCount: Long): ByteString =
originalSource.readByteString(byteCount)
override fun select(options: Options) = originalSource.select(options)
override fun readByteArray(): ByteArray = originalSource.readByteArray()
override fun readByteArray(byteCount: Long): ByteArray =
originalSource.readByteArray(byteCount)
override fun read(sink: ByteArray) = originalSource.read(sink)
override fun readFully(sink: ByteArray) = originalSource.readFully(sink)
override fun read(sink: ByteArray, offset: Int, byteCount: Int) =
originalSource.read(sink, offset, byteCount)
override fun readFully(sink: Buffer, byteCount: Long) =
originalSource.readFully(sink, byteCount)
override fun readAll(sink: Sink) = originalSource.readAll(sink)
override fun readUtf8(): String = originalSource.readUtf8()
override fun readUtf8(byteCount: Long): String = originalSource.readUtf8(byteCount)
override fun readUtf8Line(): String? = originalSource.readUtf8Line()
override fun readUtf8LineStrict(): String = originalSource.readUtf8LineStrict()
override fun readUtf8LineStrict(limit: Long): String =
originalSource.readUtf8LineStrict(limit)
override fun readUtf8CodePoint() = originalSource.readUtf8CodePoint()
override fun readString(charset: Charset): String = originalSource.readString(charset)
override fun readString(byteCount: Long, charset: Charset): String =
originalSource.readString(byteCount, charset)
override fun indexOf(b: Byte) = originalSource.indexOf(b)
override fun indexOf(b: Byte, fromIndex: Long) = originalSource.indexOf(b, fromIndex)
override fun indexOf(b: Byte, fromIndex: Long, toIndex: Long) =
originalSource.indexOf(b, fromIndex, toIndex)
override fun indexOf(bytes: ByteString) = originalSource.indexOf(bytes)
override fun indexOf(bytes: ByteString, fromIndex: Long) =
originalSource.indexOf(bytes, fromIndex)
override fun indexOfElement(targetBytes: ByteString) =
originalSource.indexOfElement(targetBytes)
override fun indexOfElement(targetBytes: ByteString, fromIndex: Long) =
originalSource.indexOfElement(targetBytes, fromIndex)
override fun rangeEquals(offset: Long, bytes: ByteString) =
originalSource.rangeEquals(offset, bytes)
override fun rangeEquals(
offset: Long,
bytes: ByteString,
bytesOffset: Int,
byteCount: Int,
) = originalSource.rangeEquals(offset, bytes, bytesOffset, byteCount)
override fun inputStream(): InputStream = originalSource.inputStream()
override fun read(sink: Buffer, byteCount: Long) = originalSource.read(sink, byteCount)
override fun timeout(): Timeout = originalSource.timeout()
override fun close() = originalSource.close()
}
override fun contentType() = originalBody.contentType()
override fun contentLength() = originalBody.contentLength()
override fun source() = wrappedSource
}

View File

@ -1,304 +0,0 @@
====================================================
tateisu/SubwayTooter
https://github.com/tateisu/SubwayTooter
Copyright (C) 2017 tateisu
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.
====================================================
square/okhttp
https://github.com/square/okhttp
Copyright (C) 2012 Square, Inc.
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.
====================================================
Commons IO
https://commons.apache.org/proper/commons-io/
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.
====================================================
chrisjenx/Calligraphy
https://github.com/chrisjenx/Calligraphy
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.
====================================================
woxblom/DragListView
https://github.com/woxblom/DragListView
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.
====================================================
omadahealth/SwipyRefreshLayout
https://github.com/omadahealth/SwipyRefreshLayout
The MIT License (MIT)
Copyright (c) 2015 OrangeGangsters
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
====================================================
kenglxn/QRGen
https://github.com/kenglxn/QRGen
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.
====================================================
sephiroth74/Android-Exif-Extended
https://github.com/sephiroth74/Android-Exif-Extended
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.
====================================================
jrummyapps/colorpicker
https://github.com/jrummyapps/colorpicker
Copyright (C) 2017 Jared Rummler
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.
====================================================
bumptech/glide
https://github.com/bumptech/glide
Copyright 2014 Google, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY GOOGLE, INC. ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those of the
authors and should not be interpreted as representing official policies, either expressed
or implied, of Google, Inc.
====================================================
iamcal/emoji-data
https://github.com/iamcal/emoji-data
Copyright (c) 2013 Cal Henderson
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
====================================================
twitter/twemoji
https://github.com/twitter/twemoji
Copyright (c) 2014 Twitter, Inc and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
====================================================
google/ExoPlayer
https://github.com/google/ExoPlayer
Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
====================================================
Icons8
https://icons8.com/license/
https://icons8.com/articles/weve-contributed-an-icon-to-imageoptim/
====================================================
OpenSticker
https://opensticker.0px.io/
(c) 2020 weep, kyori19, cutls
====================================================
BigBadaboom/AndroidSVG
https://github.com/BigBadaboom/androidsvg
Copyright 2014 Paul LeBeau, Cave Rock Software Ltd.
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.
====================================================
google/Conscrypt
https://github.com/google/conscrypt
Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
====================================================

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,14 @@
package jp.juggler.subwaytooter
import org.junit.Test
import org.junit.Assert.*
import kotlin.test.Test
import kotlin.test.assertEquals
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
assertEquals(
expected = 4,
actual = 2 + 2,
)
}
}

View File

@ -1,14 +1,18 @@
package jp.juggler.subwaytooter
import org.junit.Assert.assertEquals
import org.junit.Test
import kotlin.test.Test
import kotlin.test.assertEquals
class TestArrayListSizeBug {
@Test
fun testArrayListSize() {
val list = ArrayList(arrayOf("c", "b", "a").toList())
assertEquals("ArrayList size", 3, list.size)
assertEquals(
expected = 3,
actual = list.size,
message = "ArrayList size",
)
}
class ArrayListDerived<E>(args: List<E>) : ArrayList<E>(args)
@ -16,7 +20,11 @@ class TestArrayListSizeBug {
@Test
fun testArrayListDerived() {
val list = ArrayListDerived(arrayOf("c", "b", "a").toList())
assertEquals("ArrayListDerived size", 3, list.size)
assertEquals(
expected = 3,
actual = list.size,
message = "ArrayListDerived size",
)
// kotlin 1.5.31で(Javaの) size() ではなく getSize() にアクセスしようとして例外を出していた
// kotlin 1.5.30では大丈夫だったが、JetPack Composeは 1.5.31を要求するのだった…。
}

View File

@ -1,8 +1,8 @@
package jp.juggler.subwaytooter
import jp.juggler.subwaytooter.table.SavedAccount
import org.junit.Assert.assertEquals
import org.junit.Test
import kotlin.test.Test
import kotlin.test.assertEquals
class TestColumnMeta {
@ -12,8 +12,12 @@ class TestColumnMeta {
val actual = columnList.createTableSql()
.joinToString(";")
val expect =
"create table if not exists access_info (_id INTEGER PRIMARY KEY,a text not null,confirm_boost integer default 1,confirm_favourite integer default 1,confirm_follow integer default 1,confirm_follow_locked integer default 1,confirm_post integer default 1,confirm_reaction integer default 1,confirm_unbookmark integer default 1,confirm_unboost integer default 1,confirm_unfavourite integer default 1,confirm_unfollow integer default 1,d text,default_sensitive integer default 0,default_text text default '',dont_hide_nsfw integer default 0,dont_show_timeout integer default 0,expand_cw integer default 0,extra_json text default null,h text not null,image_max_megabytes text default null,image_resize text default null,is_misskey integer default 0,last_notification_error text,last_push_endpoint text,last_subscription_error text,max_toot_chars integer default 0,movie_max_megabytes text default null,notification_boost integer default 1,notification_favourite integer default 1,notification_follow integer default 1,notification_follow_request integer default 1,notification_mention integer default 1,notification_post integer default 1,notification_reaction integer default 1,notification_server text default '',notification_update integer default 1,notification_vote integer default 1,push_policy text default null,register_key text default '',register_time integer default 0,sound_uri text default '',t text not null,u text not null,visibility text);create index if not exists access_info_user on access_info(u);create index if not exists access_info_host on access_info(h,u)"
assertEquals("SavedAccount createParams()", expect, actual)
"create table if not exists access_info (_id INTEGER PRIMARY KEY,a text not null,confirm_boost integer default 1,confirm_favourite integer default 1,confirm_follow integer default 1,confirm_follow_locked integer default 1,confirm_post integer default 1,confirm_reaction integer default 1,confirm_unbookmark integer default 1,confirm_unboost integer default 1,confirm_unfavourite integer default 1,confirm_unfollow integer default 1,d text,default_sensitive integer default 0,default_text text default '',dont_hide_nsfw integer default 0,dont_show_timeout integer default 0,expand_cw integer default 0,extra_json text default null,h text not null,image_max_megabytes text default null,image_resize text default null,is_misskey integer default 0,max_toot_chars integer default 0,movie_max_megabytes text default null,notification_boost integer default 1,notification_favourite integer default 1,notification_follow integer default 1,notification_follow_request integer default 1,notification_mention integer default 1,notification_post integer default 1,notification_reaction integer default 1,notification_server text default '',notification_update integer default 1,notification_vote integer default 1,push_policy text default null,t text not null,u text not null,visibility text);create index if not exists access_info_user on access_info(u);create index if not exists access_info_host on access_info(h,u)"
assertEquals(
expected = expect,
actual = actual,
message = "SavedAccount createParams()",
)
}
@Test
@ -23,8 +27,8 @@ class TestColumnMeta {
2 to "alter table access_info add column notification_boost integer default 1;alter table access_info add column notification_favourite integer default 1;alter table access_info add column notification_follow integer default 1;alter table access_info add column notification_mention integer default 1",
10 to "alter table access_info add column confirm_follow integer default 1;alter table access_info add column confirm_follow_locked integer default 1;alter table access_info add column confirm_post integer default 1;alter table access_info add column confirm_unfollow integer default 1",
13 to "alter table access_info add column notification_server text default ''",
14 to "alter table access_info add column register_key text default '';alter table access_info add column register_time integer default 0",
16 to "alter table access_info add column sound_uri text default ''",
//使われなくなった 14 to "alter table access_info add column register_key text default '';alter table access_info add column register_time integer default 0",
//使われなくなった 16 to "alter table access_info add column sound_uri text default ''",
18 to "alter table access_info add column dont_show_timeout integer default 0",
23 to "alter table access_info add column confirm_favourite integer default 1",
24 to "alter table access_info add column confirm_unboost integer default 1;alter table access_info add column confirm_unfavourite integer default 1",
@ -33,21 +37,28 @@ class TestColumnMeta {
33 to "alter table access_info add column notification_reaction integer default 1;alter table access_info add column notification_vote integer default 1",
38 to "alter table access_info add column default_sensitive integer default 0;alter table access_info add column expand_cw integer default 0",
39 to "alter table access_info add column max_toot_chars integer default 0",
42 to "alter table access_info add column last_notification_error text",
//使われなくなった 42 to "alter table access_info add column last_notification_error text",
44 to "alter table access_info add column notification_follow_request integer default 1",
45 to "alter table access_info add column last_subscription_error text",
46 to "alter table access_info add column last_push_endpoint text",
//使われなくなった 45 to "alter table access_info add column last_subscription_error text",
//使われなくなった 46 to "alter table access_info add column last_push_endpoint text",
56 to "alter table access_info add column d text",
57 to "alter table access_info add column notification_post integer default 1",
59 to "alter table access_info add column image_max_megabytes text default null;alter table access_info add column image_resize text default null;alter table access_info add column movie_max_megabytes text default null",
60 to "alter table access_info add column push_policy text default null",
61 to "alter table access_info add column confirm_reaction integer default 1",
62 to "alter table access_info add column confirm_unbookmark integer default 1",
63 to "alter table access_info add column extra_json text default null",
64 to "alter table access_info add column notification_update integer default 1",
)
for (newVersion in 1..expectMap.maxOf { it.key }) {
val actualSql = columnList.upgradeSql(db = null, newVersion - 1, newVersion)
.joinToString(";")
val expectSql = expectMap[newVersion] ?: ""
assertEquals("SavedAccount v$newVersion", expectSql, actualSql)
assertEquals(
expected = expectSql,
actual = actualSql,
message = "SavedAccount v$newVersion",
)
}
}
}

View File

@ -1,8 +1,8 @@
package jp.juggler.subwaytooter
import org.junit.Assert.assertEquals
import org.junit.Test
import java.net.IDN
import kotlin.test.Test
import kotlin.test.assertEquals
@Suppress("MemberNameEqualsClassName")
class TestIDN {
@ -11,22 +11,49 @@ class TestIDN {
@Throws(Exception::class)
fun testIDN() {
// normal conversion
assertEquals("xn--3-pfuzbe6htf.juggler.jp", IDN.toASCII("マストドン3.juggler.jp", IDN.ALLOW_UNASSIGNED))
assertEquals("マストドン3.juggler.jp", IDN.toUnicode("xn--3-pfuzbe6htf.juggler.jp", IDN.ALLOW_UNASSIGNED))
assertEquals(
expected = "xn--3-pfuzbe6htf.juggler.jp",
actual = IDN.toASCII("マストドン3.juggler.jp", IDN.ALLOW_UNASSIGNED)
)
assertEquals(
expected = "マストドン3.juggler.jp",
actual = IDN.toUnicode("xn--3-pfuzbe6htf.juggler.jp", IDN.ALLOW_UNASSIGNED)
)
// not IDN domain
assertEquals("mastodon.juggler.jp", IDN.toASCII("mastodon.juggler.jp", IDN.ALLOW_UNASSIGNED))
assertEquals("mastodon.juggler.jp", IDN.toUnicode("mastodon.juggler.jp", IDN.ALLOW_UNASSIGNED))
assertEquals(
expected = "mastodon.juggler.jp",
actual = IDN.toASCII("mastodon.juggler.jp", IDN.ALLOW_UNASSIGNED)
)
assertEquals(
"mastodon.juggler.jp",
actual = IDN.toUnicode("mastodon.juggler.jp", IDN.ALLOW_UNASSIGNED)
)
// 既に変換済みの引数
assertEquals("xn--3-pfuzbe6htf.juggler.jp", IDN.toASCII("xn--3-pfuzbe6htf.juggler.jp", IDN.ALLOW_UNASSIGNED))
assertEquals("マストドン3.juggler.jp", IDN.toUnicode("マストドン3.juggler.jp", IDN.ALLOW_UNASSIGNED))
assertEquals(
expected = "xn--3-pfuzbe6htf.juggler.jp",
actual = IDN.toASCII("xn--3-pfuzbe6htf.juggler.jp", IDN.ALLOW_UNASSIGNED)
)
assertEquals(
expected = "マストドン3.juggler.jp",
actual = IDN.toUnicode("マストドン3.juggler.jp", IDN.ALLOW_UNASSIGNED)
)
// 複数のpunycode
assertEquals("թութ.հայ", IDN.toUnicode("xn--69aa8bzb.xn--y9a3aq", IDN.ALLOW_UNASSIGNED))
assertEquals(
expected = "թութ.հայ",
actual = IDN.toUnicode("xn--69aa8bzb.xn--y9a3aq", IDN.ALLOW_UNASSIGNED)
)
// ?
assertEquals("?", IDN.toASCII("?", IDN.ALLOW_UNASSIGNED))
assertEquals("?", IDN.toUnicode("?", IDN.ALLOW_UNASSIGNED))
assertEquals(
expected = "?",
actual = IDN.toASCII("?", IDN.ALLOW_UNASSIGNED),
)
assertEquals(
expected = "?",
actual = IDN.toUnicode("?", IDN.ALLOW_UNASSIGNED),
)
}
}

View File

@ -1,9 +1,15 @@
package jp.juggler.subwaytooter
import jp.juggler.util.data.*
import org.junit.Assert.assertEquals
import org.junit.Test
import jp.juggler.util.data.buildJsonArray
import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.data.decodeJsonObject
import jp.juggler.util.data.decodeJsonValue
import jp.juggler.util.data.isDecimalNotation
import jp.juggler.util.data.jsonObjectOf
import jp.juggler.util.data.writeJsonValue
import java.io.StringWriter
import kotlin.test.Test
import kotlin.test.assertEquals
class TestJson {
companion object {
@ -103,7 +109,11 @@ class TestJson {
fun x(expect: Boolean, n: Number) {
val encoded = n.toString()
val actual = encoded.isDecimalNotation()
assertEquals("${n.javaClass.simpleName} $n", expect, actual)
assertEquals(
expected = expect,
actual = actual,
message = "${n.javaClass.simpleName} $n",
)
}
// integers and longs
x(false, 0)
@ -141,8 +151,16 @@ class TestJson {
val encodedObject = jsonObjectOf("n" to n).toString()
val decodedObject = encodedObject.decodeJsonObject()
val decoded = decodedObject["n"]
assertEquals("$n type $encodedObject", expectClass, decoded?.javaClass)
assertEquals("$n value $encodedObject", expectValue, decoded)
assertEquals<Class<*>?>(
expected = expectClass,
actual = decoded?.javaClass,
message = "$n type $encodedObject",
)
assertEquals(
expected = expectValue,
actual = decoded,
message = "$n value $encodedObject",
)
}
x(0)
x(0f, expectValue = 0)

View File

@ -1,17 +1,16 @@
@file:Suppress(
"USELESS_CAST", "unused", "DEPRECATED_IDENTITY_EQUALS", "UNUSED_VARIABLE",
"UNUSED_VALUE", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE", "VARIABLE_WITH_REDUNDANT_INITIALIZER",
"ReplaceCallWithComparison"
)
package jp.juggler.subwaytooter
import org.junit.Test
import kotlin.test.Test
//import kotlin.test.*
typealias TestLambdaCallback = (x: Int) -> Int
@Suppress(
"unused", "UNUSED_VARIABLE",
"UNUSED_VALUE", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE",
"ReplaceCallWithComparison"
)
class TestKotlinFeature {
private val CODE_A = 1

View File

@ -2,8 +2,8 @@ package jp.juggler.subwaytooter
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.util.data.asciiPatternString
import org.junit.Assert.assertEquals
import org.junit.Test
import kotlin.test.Test
import kotlin.test.assertEquals
@Suppress("MemberNameEqualsClassName")
class TestMisskeyMention {

View File

@ -1,9 +1,9 @@
package jp.juggler.subwaytooter
import jp.juggler.subwaytooter.util.PostImpl
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class TestPostImplTagAscii {
@Test

View File

@ -1,16 +1,13 @@
@file:Suppress(
"USELESS_CAST", "unused", "DEPRECATED_IDENTITY_EQUALS", "UNUSED_VARIABLE",
"UNUSED_VALUE", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE", "VARIABLE_WITH_REDUNDANT_INITIALIZER",
"ReplaceCallWithComparison"
)
package jp.juggler.subwaytooter
import jp.juggler.subwaytooter.util.VersionString
import org.junit.Test
import org.junit.Assert.*
import kotlin.test.Test
import kotlin.test.assertTrue
@Suppress(
"unused",
"ReplaceCallWithComparison"
)
class TestVersionString {
@Test
fun test1() {

View File

@ -69,107 +69,122 @@ dependencies {
//noinspection GradleDependency
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:${Vers.desugarLibVersion}")
api("androidx.appcompat:appcompat:${Vers.appcompatVersion}")
api("androidx.browser:browser:1.8.0")
api("androidx.core:core-ktx:${Vers.coreKtxVersion}")
api("androidx.drawerlayout:drawerlayout:1.2.0")
api("androidx.emoji2:emoji2-bundled:${Vers.emoji2Version}")
api("androidx.emoji2:emoji2-views-helper:${Vers.emoji2Version}")
api("androidx.emoji2:emoji2-views:${Vers.emoji2Version}")
api("androidx.emoji2:emoji2:${Vers.emoji2Version}")
api("androidx.exifinterface:exifinterface:1.3.7")
api("androidx.lifecycle:lifecycle-common-java8:${Vers.lifecycleVersion}")
api("androidx.lifecycle:lifecycle-livedata-ktx:${Vers.lifecycleVersion}")
api("androidx.lifecycle:lifecycle-process:${Vers.lifecycleVersion}")
api("androidx.lifecycle:lifecycle-reactivestreams-ktx:${Vers.lifecycleVersion}")
api("androidx.lifecycle:lifecycle-runtime-ktx:${Vers.lifecycleVersion}")
api("androidx.lifecycle:lifecycle-service:${Vers.lifecycleVersion}")
api("androidx.lifecycle:lifecycle-viewmodel-ktx:${Vers.lifecycleVersion}")
api("androidx.lifecycle:lifecycle-viewmodel-savedstate:${Vers.lifecycleVersion}")
api("androidx.recyclerview:recyclerview:1.3.2")
api("androidx.startup:startup-runtime:${Vers.startupVersion}")
api("androidx.work:work-runtime-ktx:${Vers.workVersion}")
api("androidx.work:work-runtime:${Vers.workVersion}")
api("com.astuetz:pagerslidingtabstrip:1.0.1")
api("com.caverock:androidsvg-aar:1.4")
api("com.github.hadilq:live-event:1.3.0")
api("com.github.omadahealth:swipy:1.2.3@aar")
api("com.github.woxthebox:draglistview:1.7.3")
api("com.google.android.flexbox:flexbox:3.0.0")
api("com.google.android.material:material:${Vers.materialVersion}")
api("com.otaliastudios:transcoder:0.10.5")
api("com.squareup.okhttp3:okhttp-urlconnection:${Vers.okhttpVersion}")
api("com.squareup.okhttp3:okhttp:${Vers.okhttpVersion}")
api("org.bouncycastle:bcprov-jdk15on:1.70")
api("org.jetbrains.kotlin:kotlin-reflect:${Vers.kotlinVersion}")
api("org.jetbrains.kotlinx:kotlinx-coroutines-android:${Vers.kotlinxCoroutinesVersion}")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Vers.kotlinxCoroutinesVersion}")
api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
api("org.jetbrains.kotlinx:kotlinx-serialization-json:${Vers.kotlinxSerializationLibVersion}")
api("ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0")
// JugglerBaseInitializer で使う
implementation("androidx.startup:startup-runtime:${Vers.androidxStartup}")
//non-OSS dependency api "androidx.media3:media3-cast:$media3Version"
api("androidx.media3:media3-common:${Vers.media3Version}")
api("androidx.media3:media3-datasource:${Vers.media3Version}")
api("androidx.media3:media3-effect:${Vers.media3Version}")
api("androidx.media3:media3-exoplayer:${Vers.media3Version}")
api("androidx.media3:media3-session:${Vers.media3Version}")
api("androidx.media3:media3-transformer:${Vers.media3Version}")
api("androidx.media3:media3-ui:${Vers.media3Version}")
// decodeP256dh で使う
implementation("org.bouncycastle:bcprov-jdk15on:1.70")
// commons-codecをapiにすると、端末上の古いjarが使われてしまう
// declaration of "org.apache.commons.codec.binary.Base64" appears in /system/framework/org.apache.http.legacy.jar)
androidTestImplementation("commons-codec:commons-codec:${Vers.commonsCodecVersion}")
// defaultSecurityProvider で使う
implementation("org.conscrypt:conscrypt-android:${Vers.conscryptVersion}")
// Koin main features for Android
api("io.insert-koin:koin-android:${Vers.koinVersion}")
api("io.insert-koin:koin-android-compat:${Vers.koinVersion}")
api("io.insert-koin:koin-androidx-workmanager:${Vers.koinVersion}")
// api( "io.insert-koin:koin-androidx-navigation:$koinVersion")
// api( "io.insert-koin:koin-androidx-compose:$koinVersion")
// AudioTranscoderで使う
implementation("androidx.media3:media3-common:${Vers.androidxMedia3}")
implementation("androidx.media3:media3-transformer:${Vers.androidxMedia3}")
implementation("androidx.media3:media3-effect:${Vers.androidxMedia3}")
// for com.bumptech.glide.integration.webp.*
api("com.github.zjupure:webpdecoder:${Vers.webpDecoderVersion}")
api("com.github.bumptech.glide:glide:${Vers.glideVersion}")
api("com.github.bumptech.glide:annotations:${Vers.glideVersion}")
api("com.github.bumptech.glide:okhttp3-integration:${Vers.glideVersion}") {
exclude("com.squareup.okhttp3", "okhttp")
}
// EmptyScope.kt で使う
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Vers.kotlinxCoroutinesVersion}")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:${Vers.androidxLifecycle}")
androidTestApi("androidx.test.espresso:espresso-core:${Vers.androidxTestEspressoCoreVersion}")
androidTestApi("androidx.test.ext:junit-ktx:1.1.5")
androidTestApi("androidx.test.ext:junit:${Vers.androidxTestExtJunitVersion}")
androidTestApi("androidx.test.ext:truth:1.5.0")
androidTestApi("androidx.test:core-ktx:${Vers.testKtxVersion}")
androidTestApi("androidx.test:core:${Vers.androidxTestVersion}")
androidTestApi("androidx.test:runner:1.5.2")
androidTestApi("org.jetbrains.kotlin:kotlin-test:${Vers.kotlinTestVersion}")
androidTestApi("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Vers.kotlinxCoroutinesVersion}")
testApi("androidx.arch.core:core-testing:${Vers.archVersion}")
testApi("junit:junit:${Vers.junitVersion}")
testApi("org.jetbrains.kotlin:kotlin-test:${Vers.kotlinTestVersion}")
testApi("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Vers.kotlinxCoroutinesVersion}")
// Compat.kt で使う
implementation("androidx.annotation:annotation:${Vers.androidxAnnotation}")
implementation("androidx.appcompat:appcompat:${Vers.androidxAppcompat}")
// To use android test orchestrator
androidTestUtil("androidx.test:orchestrator:1.4.2")
// JsonDelegate で使う
implementation(kotlin("reflect"))
// UriSerializer で使う。アカウント設定で状態の保存に kotlinx-serialization-json を使っている
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${Vers.kotlinxSerializationLibVersion}")
testApi("com.squareup.okhttp3:mockwebserver:${Vers.okhttpVersion}") {
exclude("com.squareup.okio", "okio")
exclude("com.squareup.okhttp3", "okhttp")
exclude("org.jetbrains.kotlin", "kotlin-stdlib-common")
exclude("org.jetbrains.kotlin", "kotlin-stdlib")
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
}
androidTestApi("com.squareup.okhttp3:mockwebserver:${Vers.okhttpVersion}") {
exclude("com.squareup.okio", "okio")
exclude("com.squareup.okhttp3", "okhttp")
exclude("org.jetbrains.kotlin", "kotlin-stdlib-common")
exclude("org.jetbrains.kotlin", "kotlin-stdlib")
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
}
// BitmapUtils で使う
implementation("androidx.exifinterface:exifinterface:1.3.7")
// conscrypt をUnitテストするための指定
// https://github.com/google/conscrypt/issues/649
// MovieUtils で使う
implementation("com.otaliastudios:transcoder:0.10.5")
api("org.conscrypt:conscrypt-android:${Vers.conscryptVersion}")
// HttpUtils で使う
implementation("com.squareup.okhttp3:okhttp:${Vers.okhttpVersion}")
// ==========================================================================
// 単体テスト
testImplementation(kotlin("test"))
// ==========================================================================
// AndroidTest
// 紛らわしいのでAndroidTestではkotlin.testを使わない androidTestImplementation(kotlin("test"))
androidTestRuntimeOnly("androidx.test:runner:1.5.2")
androidTestImplementation("androidx.test:core:${Vers.androidxTestCore}")
androidTestImplementation("androidx.test.ext:junit:${Vers.androidxTestExtJunit}")
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Vers.kotlinxCoroutinesVersion}")
// DispatchersTest で使う
androidTestImplementation("androidx.lifecycle:lifecycle-viewmodel-ktx:${Vers.androidxLifecycle}")
// implementation("androidx.core:core-ktx:${Vers.androidxCoreVersion}")
//
// implementation("androidx.emoji2:emoji2-bundled:${Vers.androidxEmoji2}")
// implementation("androidx.emoji2:emoji2-views-helper:${Vers.androidxEmoji2}")
// implementation("androidx.emoji2:emoji2-views:${Vers.androidxEmoji2}")
// implementation("androidx.emoji2:emoji2:${Vers.androidxEmoji2}")
// implementation("androidx.lifecycle:lifecycle-common-java8:${Vers.lifecycleVersion}")
// implementation("androidx.lifecycle:lifecycle-livedata-ktx:${Vers.lifecycleVersion}")
// implementation("androidx.lifecycle:lifecycle-process:${Vers.lifecycleVersion}")
// implementation("androidx.lifecycle:lifecycle-reactivestreams-ktx:${Vers.lifecycleVersion}")
// implementation("androidx.lifecycle:lifecycle-service:${Vers.lifecycleVersion}")
// implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:${Vers.lifecycleVersion}")
// implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:${Vers.lifecycleVersion}")
//
//
// implementation("com.astuetz:pagerslidingtabstrip:1.0.1")
// implementation("com.caverock:androidsvg-aar:1.4")
// implementation("com.github.UnifiedPush:android-connector:2.1.1")
//
// implementation("com.github.bumptech.glide:annotations:${Vers.glideVersion}")
// implementation("com.github.bumptech.glide:glide:${Vers.glideVersion}")
// implementation("com.github.hadilq:live-event:1.3.0")
//
// implementation("com.github.penfeizhou.android.animation:apng:${Vers.apng4AndroidVersion}")
//
// implementation("com.github.zjupure:webpdecoder:${Vers.webpDecoderVersion}")
//
// implementation("com.google.android.material:material:${Vers.googleMaterialVersion}")
//
// implementation("jp.wasabeef:glide-transformations:4.3.0")
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:${Vers.kotlinxCoroutinesVersion}")
// implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
// implementation("ru.gildor.coroutines:kotlin-coroutines-okhttp:${Vers.gildorkotlinCoroutinesOkhttp}")
// androidTestImplementation("androidx.test.espresso:espresso-core:${Vers.androidxTestEspressoCoreVersion}")
// androidTestImplementation("androidx.test.ext:junit-ktx:1.1.5")
// androidTestImplementation("androidx.test.ext:truth:1.5.0")
// androidTestImplementation("androidx.test:core-ktx:${Vers.testKtxVersion}")
//
// testImplementation("androidx.arch.core:core-testing:${Vers.archVersion}")
// testImplementation("junit:junit:${Vers.junitVersion}")
// testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Vers.kotlinxCoroutinesVersion}")
//
// // Compose compilerによりkotlinのバージョンを上げられない
// //noinspection GradleDependency
// testImplementation("org.jetbrains.kotlin:kotlin-test:${Vers.kotlinTestVersion}")
// //noinspection GradleDependency
// androidTestImplementation("org.jetbrains.kotlin:kotlin-test:${Vers.kotlinTestVersion}")
//
// // To use android test orchestrator
// // androidTestUtil("androidx.test:orchestrator:1.4.2")
//
// testImplementation("com.squareup.okhttp3:mockwebserver:${Vers.okhttpVersion}") {
// exclude("com.squareup.okio", "okio")
// exclude("com.squareup.okhttp3", "okhttp")
// exclude("org.jetbrains.kotlin", "kotlin-stdlib-common")
// exclude("org.jetbrains.kotlin", "kotlin-stdlib")
// exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
// }
// androidTestImplementation("com.squareup.okhttp3:mockwebserver:${Vers.okhttpVersion}") {
// exclude("com.squareup.okio", "okio")
// exclude("com.squareup.okhttp3", "okhttp")
// exclude("org.jetbrains.kotlin", "kotlin-stdlib-common")
// exclude("org.jetbrains.kotlin", "kotlin-stdlib")
// exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
// }
}

View File

@ -4,55 +4,64 @@ import jp.juggler.crypt.toByteRange
import jp.juggler.util.data.decodeBase64
import jp.juggler.util.data.encodeBase64
import jp.juggler.util.data.encodeBase64Url
import org.apache.commons.codec.binary.Base64.encodeBase64String
import org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Test
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
class ByteRangeTest {
/**
* ByteRangeや StringUtilsのBase64がcommons-codecの出力結果と一致するか調べる
* ByteRangeや StringUtilsのBase64がkotlin.io.encoding.Base64 の出力結果と一致するか調べる
*/
@OptIn(ExperimentalEncodingApi::class)
@Test
fun testByteRangeBase64() {
for (len in 0..300) {
val src = ByteArray(len) { it.toByte() }
run {
val encodedByApacheCodec = encodeBase64URLSafeString(src)
val kotlinBase64UrlSafe = Base64.UrlSafe
// kotlin.io の Base64.UrlSafe は 末尾の = パディングを残すので後から除去する必要がある
val encodedByKotlinIo = kotlinBase64UrlSafe.encode(src).trimEnd { it=='=' }
// ByteRange().encodeBase64Url() はパディングを含まない
val encodeByByteRange = src.toByteRange().encodeBase64Url()
val encodeByUtils = src.encodeBase64Url()
val decodedByUtils = encodeByUtils.decodeBase64()
// StringUtils の encodeBase64Url() はパディングを含まない
val encodeByStringUtils = src.encodeBase64Url()
// もちろんStringUtilsの decodeBase64() でデコードできる
val decodedByUtils = encodeByStringUtils.decodeBase64()
assertEquals(
"len=$len",
encodedByApacheCodec,
encodedByKotlinIo,
encodeByByteRange,
)
assertEquals(
"len=$len",
encodedByApacheCodec,
encodeByUtils,
encodedByKotlinIo,
encodeByStringUtils,
)
assertArrayEquals(
"len=$len encoded=$encodeByUtils",
"len=$len encoded=$encodeByStringUtils",
src,
decodedByUtils,
)
}
run {
val encodedByApacheCodec = encodeBase64String(src)
val kotinBase64 = Base64.Default
val encodedByKotlinIo = kotinBase64.encode(src)
val encodeByByteRange = src.toByteRange().encodeBase64()
val encodeByUtils = src.encodeBase64()
val decodedByUtils = encodeByUtils.decodeBase64()
assertEquals(
"len=$len",
encodedByApacheCodec,
encodedByKotlinIo,
encodeByByteRange,
)
assertEquals(
"len=$len",
encodedByApacheCodec,
encodedByKotlinIo,
encodeByUtils,
)
assertArrayEquals(

View File

@ -8,7 +8,10 @@ import jp.juggler.util.coroutine.AppDispatchers
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.*
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.Assert.*

View File

@ -9,7 +9,6 @@ import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import androidx.annotation.RawRes
import jp.juggler.util.log.LogCategory
import okhttp3.internal.closeQuietly
import java.io.InputStream
private val log = LogCategory("StorageUtils")
@ -223,7 +222,10 @@ fun getStreamSize(bClose: Boolean, inStream: InputStream): Long {
}
return size
} finally {
if (bClose) inStream.closeQuietly()
if (bClose) try {
inStream.close()
} catch (_: Throwable) {
}
}
}

View File

@ -1,5 +1,5 @@
/**
* kotlinx-datetime を使った日時関連のユーティリティ
* Java8 Time API を使った日時関連のユーティリティ
* - api "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0"
* - desugar Java 8 の日時APIを使えるようにする必要がある
* - https://developer.android.com/studio/write/java8-support?hl=ja
@ -9,9 +9,8 @@
package jp.juggler.util.time
import jp.juggler.util.log.LogCategory
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import java.time.Instant
import java.time.ZoneId
private val log = LogCategory("TimeUtils")
@ -22,7 +21,7 @@ fun String.parseTimeIso8601() =
when {
isBlank() -> null
else -> try {
Instant.parse(this).toEpochMilliseconds()
Instant.parse(this).toEpochMilli()
} catch (ex: Throwable) {
log.w("parseTime failed. $this")
null
@ -33,15 +32,15 @@ fun String.parseTimeIso8601() =
* UI表示用フォーマットは適当
*/
fun Long.formatLocalTime(): String {
val tz = TimeZone.currentSystemDefault()
val lt = Instant.fromEpochMilliseconds(this).toLocalDateTime(tz)
val tz = ZoneId.systemDefault()
val zdt = Instant.ofEpochMilli(this).atZone(tz)
return "%d/%02d/%02d %02d:%02d:%02d.%03d".format(
lt.year,
lt.monthNumber,
lt.dayOfMonth,
lt.hour,
lt.minute,
lt.second,
lt.nanosecond / 1_000_000,
zdt.year,
zdt.monthValue,
zdt.dayOfMonth,
zdt.hour,
zdt.minute,
zdt.second,
zdt.nano / 1_000_000,
)
}

View File

@ -23,6 +23,7 @@ import android.util.SparseArray
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
@ -351,6 +352,10 @@ fun AppCompatActivity.setNavigationBack(toolbar: Toolbar) =
toolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
fun ComponentActivity.setNavigationBack(toolbar: Toolbar) =
toolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
val Float.roundPixels get() = (this + 0.5f).toInt()
fun DisplayMetrics.dpFloat(src: Float) = (density * src)

View File

@ -129,7 +129,7 @@ private fun rgbToLab(rgb: Int): Triple<Float, Float, Float> {
)
}
fun AppCompatActivity.setStatusBarColorCompat(@ColorInt c: Int) {
fun Activity.setStatusBarColorCompat(@ColorInt c: Int) {
window?.apply {
statusBarColor = Color.BLACK or c
@ -157,7 +157,7 @@ fun AppCompatActivity.setStatusBarColorCompat(@ColorInt c: Int) {
}
}
fun AppCompatActivity.setNavigationBarColorCompat(@ColorInt c: Int) {
fun Activity.setNavigationBarColorCompat(@ColorInt c: Int) {
if (c == 0) {
// no way to restore to system default, need restart app.
return

View File

@ -1,10 +1,10 @@
package jp.juggler.pushreceiverapp
package jp.juggler
import jp.juggler.util.data.Base128.decodeBase128
import jp.juggler.util.data.Base128.encodeBase128
import org.junit.Assert.assertArrayEquals
import org.junit.Test
import java.io.ByteArrayOutputStream
import kotlin.test.Test
import kotlin.test.assertContentEquals
class Base128Test {
@ -20,10 +20,10 @@ class Base128Test {
}.toByteArray()
val encoded = orig.encodeBase128()
val decoded = encoded.decodeBase128()
assertArrayEquals(
"len=$len,i=$i",
assertContentEquals(
orig,
decoded
decoded,
"len=$len,i=$i",
)
}
}

View File

@ -4,8 +4,11 @@ import jp.juggler.util.data.BinPackList
import jp.juggler.util.data.BinPackMap
import jp.juggler.util.data.decodeBinPack
import jp.juggler.util.data.encodeBinPack
import org.junit.Assert.*
import org.junit.Test
import kotlin.test.DefaultAsserter.assertEquals
import kotlin.test.DefaultAsserter.assertNotNull
import kotlin.test.DefaultAsserter.assertTrue
import kotlin.test.Test
import kotlin.test.assertContentEquals
class BinPackTest {
@Test
@ -17,11 +20,12 @@ class BinPackTest {
val decoded = encoded.decodeBinPack()
val message = "($v ${v?.javaClass?.simpleName}) dump=${encoded.dump()}"
when {
expected is ByteArray -> assertArrayEquals(
"${v?.javaClass?.simpleName} $v",
expected is ByteArray -> assertContentEquals(
expected,
decoded as? ByteArray
decoded as? ByteArray,
"${v?.javaClass?.simpleName} $v",
)
v is Set<*> -> {
val decodedSet = (decoded as? BinPackList)?.toSet()
assertNotNull("$message decoded?", decodedSet)
@ -29,6 +33,7 @@ class BinPackTest {
assertTrue("$message containsAll 1", v.containsAll(decodedSet))
assertTrue("$message containsAll 2", decodedSet.containsAll(v))
}
else -> assertEquals(
message,
expected,

View File

@ -1,7 +1,7 @@
package jp.juggler.base
package jp.juggler
import org.junit.Test
import org.junit.Assert.*
import kotlin.test.Test
import kotlin.test.assertEquals
class ExampleUnitTest {
@Test

View File

@ -5,13 +5,16 @@ buildscript {
}
dependencies {
classpath("com.android.tools.build:gradle:${Vers.androidGradlePruginVersion}")
classpath("com.android.tools.build:gradle:${Vers.androidGradlePrugin}")
// room のバージョンの影響で google-services を上げられない場合がある
classpath("com.google.gms:google-services:4.4.1")
//noinspection DifferentKotlinGradleVersion
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Vers.kotlinVersion}")
// Compose Compilerの都合でkotlinを上げられない場合がある
//noinspection GradleDependency
classpath("org.jetbrains.kotlin:kotlin-serialization:${Vers.kotlinVersion}")
classpath("com.github.bjoernq:unmockplugin:0.7.6")
@ -25,6 +28,7 @@ plugins {
kotlin("plugin.serialization") version (Vers.kotlinxSerializationPluginVersion) apply true // !!
id("org.jetbrains.kotlin.android") version (Vers.kotlinVersion) apply false
id("com.google.devtools.ksp") version (Vers.kspVersion) apply false
// id("com.android.library") version "8.3.0" apply false
}
allprojects {

View File

@ -2,54 +2,64 @@ import org.gradle.api.JavaVersion
@Suppress("ConstPropertyName")
object Vers {
const val stBuildToolsVersion = "34.0.0"
const val stCompileSdkVersion = 34
const val stTargetSdkVersion = 34
const val stMinSdkVersion = 26
val javaSourceCompatibility = JavaVersion.VERSION_1_8
val javaTargetCompatibility = JavaVersion.VERSION_1_8
val javaSourceCompatibility = JavaVersion.VERSION_19
val javaTargetCompatibility = JavaVersion.VERSION_19
// Compose Compiler 1.5.10 は kotlin 1.9.22 を要求する
@Suppress("MemberVisibilityCanBePrivate")
const val kotlinVersion = "1.9.23"
const val kotlinJvmTarget = "1.8"
const val kotlinJvmToolchain = 17
const val androidGradlePruginVersion = "8.3.0"
const val androidxAnnotationVersion = "1.6.0"
const val androidxTestEspressoCoreVersion = "3.5.1"
const val androidxTestExtJunitVersion = "1.1.5"
const val androidxTestVersion = "1.5.0"
const val kotlinVersion = "1.9.22"
@Suppress("MemberVisibilityCanBePrivate")
const val glideVersion = "4.15.1"
// Compose Compiler 1.5.10 は jvmTarget = "19" を要求する
// しかし Android Studio 自体は17で動いてるので単体テスト時に問題がでる
const val kotlinJvmTarget = "19"
const val kotlinJvmToolchain = 19
const val androidGradlePrugin = "8.3.0"
// const val ankoVersion = "0.10.8"
const val appcompatVersion = "1.6.1"
const val archVersion = "2.2.0"
const val benManesVersion = "0.51.0"
const val commonsCodecVersion = "1.16.0"
const val composeVersion = "1.0.5"
// const val commonsCodecVersion = "1.16.0"
// const val composeVersion = "1.0.5"
const val androidxActivity = "1.8.2"
const val androidxAnnotation = "1.7.1"
const val androidxAppcompat = "1.6.1"
const val androidxArchCoreTesting = "2.2.0"
const val androidxComposeRuntime = "1.6.3"
const val androidxComposeUi = "1.6.3"
const val androidxCore = "1.12.0"
const val androidxEmoji2 = "1.4.0"
const val androidxLifecycle = "2.7.0"
const val androidxMedia3 = "1.3.0"
const val androidxPreferenceKtx = "1.2.1"
const val androidxRecyclerView = "1.3.2"
const val androidxStartup = "1.1.1"
const val androidxTestCore = "1.5.0"
const val androidxTestCoreKtx = "1.5.0"
const val androidxTestEspressoCore = "3.5.1"
const val androidxTestExtJunit = "1.1.5"
const val androidxWork = "2.9.0"
const val apng4AndroidVersion = "2.25.0"
const val conscryptVersion = "2.5.2"
const val coreKtxVersion = "1.12.0"
const val desugarLibVersion = "2.0.4"
const val detektVersion = "1.23.4"
const val emoji2Version = "1.4.0"
const val gildorkotlinCoroutinesOkhttp = "1.0"
const val googleFlexbox="3.0.0"
const val googleMaterial = "1.11.0"
const val junitVersion = "4.13.2"
const val koinVersion = "3.5.0"
const val kotlinTestVersion = kotlinVersion
const val kotlinxCoroutinesVersion = "1.8.0"
const val kotlinxSerializationPluginVersion = kotlinVersion
const val kotlinxSerializationLibVersion = "1.6.3"
const val kspVersion = "$kotlinVersion-1.0.19"
const val lifecycleVersion = "2.7.0"
const val materialVersion = "1.11.0"
const val media3Version = "1.3.0"
const val okhttpVersion = "5.0.0-alpha.11"
const val preferenceKtxVersion = "1.2.1"
const val startupVersion = "1.1.1"
const val testKtxVersion = "1.5.0"
const val kotlinxSerializationPluginVersion = kotlinVersion
const val kspVersion = "$kotlinVersion-1.0.17"
const val okhttpVersion = "5.0.0-alpha.12"
const val webpDecoderVersion = "2.6.$glideVersion"
const val workVersion = "2.9.0"
}

View File

@ -23,6 +23,9 @@ android {
lint {
abortOnError = false
}
buildFeatures {
viewBinding = true
}
compileOptions {
sourceCompatibility = Vers.javaSourceCompatibility
@ -52,5 +55,12 @@ android {
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:${Vers.desugarLibVersion}")
// dismissSafe, systemService, View.gone() など
implementation(project(":base"))
implementation("androidx.core:core-ktx:${Vers.androidxCore}")
implementation("androidx.appcompat:appcompat:${Vers.androidxAppcompat}")
implementation("androidx.annotation:annotation:${Vers.androidxAnnotation}")
implementation("com.google.android.flexbox:flexbox:${Vers.googleFlexbox}")
}

View File

@ -1,94 +0,0 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jrummyapps.android.colorpicker
import android.graphics.*
import android.graphics.drawable.Drawable
import kotlin.math.ceil
/**
* This drawable will draw a simple white and gray chessboard pattern.
* It's the pattern you will often see as a background behind a partly transparent image in many applications.
*/
internal class AlphaPatternDrawable(private val rectangleSize: Int) : Drawable() {
private val paint = Paint()
private val paintWhite = Paint().apply { color = Color.WHITE }
private val paintGray = Paint().apply { color = -0x343434 }
private var numRectanglesHorizontal = 0
private var numRectanglesVertical = 0
/**
* Bitmap in which the pattern will be cached.
* This is so the pattern will not have to be recreated each time draw() gets called.
* Because recreating the pattern i rather expensive. I will only be recreated if the size changes.
*/
private var bitmap: Bitmap? = null
/**
* This will generate a bitmap with the pattern as big as the rectangle we were allow to draw on.
* We do this to chache the bitmap so we don't need to recreate it each time draw() is called since it takes a few milliseconds
*/
private fun generatePatternBitmap() {
if (bounds.width() <= 0 || bounds.height() <= 0) {
return
}
val bitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888)
.also { this.bitmap = it }
val canvas = Canvas(bitmap)
val r = Rect()
var verticalStartWhite = true
for (i in 0..numRectanglesVertical) {
var isWhite = verticalStartWhite
for (j in 0..numRectanglesHorizontal) {
r.top = i * rectangleSize
r.left = j * rectangleSize
r.bottom = r.top + rectangleSize
r.right = r.left + rectangleSize
canvas.drawRect(r, if (isWhite) paintWhite else paintGray)
isWhite = !isWhite
}
verticalStartWhite = !verticalStartWhite
}
}
override fun draw(canvas: Canvas) {
val bitmap = this.bitmap
if (bitmap != null && !bitmap.isRecycled) {
canvas.drawBitmap(bitmap, null, bounds, paint)
}
}
override fun getOpacity(): Int {
return PixelFormat.UNKNOWN
}
override fun setAlpha(alpha: Int) {
throw UnsupportedOperationException("Alpha is not supported by this drawable.")
}
override fun setColorFilter(cf: ColorFilter?) {
throw UnsupportedOperationException("ColorFilter is not supported by this drawable.")
}
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
val height = bounds.height()
val width = bounds.width()
numRectanglesHorizontal = ceil((width / rectangleSize.toFloat()).toDouble()).toInt()
numRectanglesVertical = ceil((height / rectangleSize.toFloat()).toDouble()).toInt()
generatePatternBitmap()
}
}

View File

@ -27,7 +27,7 @@ import androidx.core.graphics.ColorUtils
internal class ColorPaletteAdapter(
val colors: IntArray,
var selectedPosition: Int,
@ColorShape val colorShape: Int,
val colorShape: ColorShape,
val listener: (Int) -> Unit,
) : BaseAdapter() {
@ -47,10 +47,9 @@ internal class ColorPaletteAdapter(
private inner class ViewHolder(parent: ViewGroup) {
val root: View = run {
LayoutInflater.from(parent.context).inflate(
if (colorShape == ColorShape.SQUARE) {
R.layout.cpv_color_item_square
} else {
R.layout.cpv_color_item_circle
when (colorShape) {
ColorShape.Square -> R.layout.cpv_color_item_square
ColorShape.Circle -> R.layout.cpv_color_item_circle
},
parent,
false
@ -81,12 +80,14 @@ internal class ColorPaletteAdapter(
imageView.colorFilter = null
}
}
alpha <= ColorPickerDialog.ALPHA_THRESHOLD -> {
alpha <= ALPHA_THRESHOLD -> {
colorPanelView.borderColor = color or -0x1000000
imageView.setColorFilter( /*color | 0xFF000000*/Color.BLACK,
PorterDuff.Mode.SRC_IN
)
}
else -> {
colorPanelView.borderColor = originalBorderColor
imageView.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)

View File

@ -16,7 +16,13 @@
package com.jrummyapps.android.colorpicker
import android.content.Context
import android.graphics.*
import android.graphics.BitmapShader
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Shader
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.os.Parcelable
@ -30,6 +36,17 @@ import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
enum class ColorShape(val attrEnum: Int) {
Square(0),
Circle(1),
;
companion object {
fun fromInt(i: Int): ColorShape =
entries.find { it.attrEnum == i } ?: Square
}
}
/**
* This class draws a panel which which will be filled with a color which can be set. It can be used to show the
* currently selected color which you will get from the [ColorPickerView].
@ -45,7 +62,7 @@ class ColorPanelView @JvmOverloads constructor(
}
/* The width in pixels of the border surrounding the color panel. */
private val borderWidthPx = DrawingUtils.dpToPx(context, 1f)
private val borderWidthPx = context.dpToPx(1f)
private val borderPaint = Paint().apply {
isAntiAlias = true
@ -68,7 +85,7 @@ class ColorPanelView @JvmOverloads constructor(
private var drawingRect = Rect()
private var colorRect = Rect()
private var alphaPattern = AlphaPatternDrawable(DrawingUtils.dpToPx(context, 4f))
private var alphaPattern = TilePatternDrawable(context.dpToPx(4f))
private var showOldColor = false
@ -88,8 +105,7 @@ class ColorPanelView @JvmOverloads constructor(
}
}
@ColorShape
private var shape = 0
private var shape = ColorShape.Circle
set(value) {
if (field != value) {
field = value
@ -99,9 +115,9 @@ class ColorPanelView @JvmOverloads constructor(
init {
val a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView)
shape = a.getInt(R.styleable.ColorPanelView_cpv_colorShape, ColorShape.CIRCLE)
shape = ColorShape.fromInt(a.getInt(R.styleable.ColorPanelView_cpv_colorShape, -1))
showOldColor = a.getBoolean(R.styleable.ColorPanelView_cpv_showOldColor, false)
check(!(showOldColor && shape != ColorShape.CIRCLE)) { "Color preview is only available in circle mode" }
check(!(showOldColor && shape != ColorShape.Circle)) { "Color preview is only available in circle mode" }
borderColor = a.getColor(R.styleable.ColorPanelView_cpv_borderColor, DEFAULT_BORDER_COLOR)
a.recycle()
@ -137,13 +153,13 @@ class ColorPanelView @JvmOverloads constructor(
override fun onDraw(canvas: Canvas) {
borderPaint.color = borderColor
colorPaint.color = color
if (shape == ColorShape.SQUARE) {
if (shape == ColorShape.Square) {
if (borderWidthPx > 0) {
canvas.drawRect(drawingRect, borderPaint)
}
alphaPattern.draw(canvas)
canvas.drawRect(colorRect, colorPaint)
} else if (shape == ColorShape.CIRCLE) {
} else if (shape == ColorShape.Circle) {
val outerRadius = measuredWidth / 2
if (borderWidthPx > 0) {
canvas.drawCircle(
@ -176,24 +192,22 @@ class ColorPanelView @JvmOverloads constructor(
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
when (shape) {
ColorShape.SQUARE -> {
ColorShape.Square -> {
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
setMeasuredDimension(width, height)
}
ColorShape.CIRCLE -> {
ColorShape.Circle -> {
super.onMeasure(widthMeasureSpec, widthMeasureSpec)
setMeasuredDimension(measuredWidth, measuredWidth)
}
else -> {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (shape == ColorShape.SQUARE || showOldColor) {
if (shape == ColorShape.Square || showOldColor) {
drawingRect.set(
paddingLeft,
paddingTop,

View File

@ -1,41 +0,0 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jrummyapps.android.colorpicker
import androidx.annotation.ColorInt
/**
* Callback used for getting the selected color from a color picker dialog.
*/
interface ColorPickerDialogListener {
/**
* Callback that is invoked when a color is selected from the color picker dialog.
*
* @param dialogId
* The dialog id used to create the dialog instance.
* @param newColor
* The selected color
*/
fun onColorSelected(dialogId: Int, @ColorInt newColor: Int)
/**
* Callback that is invoked when the color picker dialog was dismissed.
*
* @param dialogId
* The dialog id used to create the dialog instance.
*/
fun onDialogDismissed(dialogId: Int)
}

View File

@ -17,8 +17,18 @@ package com.jrummyapps.android.colorpicker
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ComposeShader
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Paint.Align
import android.graphics.Point
import android.graphics.PorterDuff
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Shader
import android.graphics.Shader.TileMode
import android.os.Bundle
import android.os.Parcelable
@ -57,7 +67,7 @@ class ColorPickerView @JvmOverloads constructor(
private const val BORDER_WIDTH_PX = 1
}
interface OnColorChangedListener {
fun interface OnColorChangedListener {
fun onColorChanged(newColor: Int)
}
@ -70,41 +80,35 @@ class ColorPickerView @JvmOverloads constructor(
/**
* The width in px of the hue panel.
*/
private val huePanelWidthPx =
DrawingUtils.dpToPx(context, HUE_PANEL_WDITH_DP.toFloat())
private val huePanelWidthPx = context.dpToPx(HUE_PANEL_WDITH_DP)
/**
* The height in px of the alpha panel
*/
private val alphaPanelHeightPx =
DrawingUtils.dpToPx(context, ALPHA_PANEL_HEIGH_DP.toFloat())
private val alphaPanelHeightPx = context.dpToPx(ALPHA_PANEL_HEIGH_DP)
/**
* The distance in px between the different
* color panels.
*/
private val panelSpacingPx =
DrawingUtils.dpToPx(context, PANEL_SPACING_DP.toFloat())
private val panelSpacingPx = context.dpToPx(PANEL_SPACING_DP)
/**
* The radius in px of the color palette tracker circle.
*/
private val circleTrackerRadiusPx =
DrawingUtils.dpToPx(context, CIRCLE_TRACKER_RADIUS_DP.toFloat())
private val circleTrackerRadiusPx = context.dpToPx(CIRCLE_TRACKER_RADIUS_DP)
/**
* The px which the tracker of the hue or alpha panel
* will extend outside of its bounds.
*/
private val sliderTrackerOffsetPx =
DrawingUtils.dpToPx(context, SLIDER_TRACKER_OFFSET_DP.toFloat())
private val sliderTrackerOffsetPx = context.dpToPx(SLIDER_TRACKER_OFFSET_DP)
/**
* Height of slider tracker on hue panel,
* width of slider on alpha panel.
*/
private val sliderTrackerSizePx =
DrawingUtils.dpToPx(context, SLIDER_TRACKER_SIZE_DP.toFloat())
private val sliderTrackerSizePx = context.dpToPx(SLIDER_TRACKER_SIZE_DP)
/**
* the current value of the text that will be shown in the alpha slider.
@ -147,7 +151,7 @@ class ColorPickerView @JvmOverloads constructor(
private val satValTrackerPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = DrawingUtils.dpToPx(context, 2f).toFloat()
strokeWidth = context.dpToPx(2f).toFloat()
isAntiAlias = true
}
@ -155,7 +159,7 @@ class ColorPickerView @JvmOverloads constructor(
private val alphaTextPaint = Paint().apply {
color = -0xe3e3e4
textSize = DrawingUtils.dpToPx(context, 14f).toFloat()
textSize = context.dpToPx(14f).toFloat()
isAntiAlias = true
textAlign = Align.CENTER
isFakeBoldText = true
@ -164,7 +168,7 @@ class ColorPickerView @JvmOverloads constructor(
private val hueAlphaTrackerPaint = Paint().apply {
color = sliderTrackerColor
style = Paint.Style.STROKE
strokeWidth = DrawingUtils.dpToPx(context, 2f).toFloat()
strokeWidth = context.dpToPx(2f).toFloat()
isAntiAlias = true
}
@ -208,7 +212,7 @@ class ColorPickerView @JvmOverloads constructor(
private var hueRect: Rect? = null
private var alphaRect: Rect? = null
private var startTouchPoint: Point? = null
private var alphaPatternDrawable: AlphaPatternDrawable? = null
private var tilePatternDrawable: TilePatternDrawable? = null
/**
* OnColorChangedListener to get notified when the color selected by the user has changed.
@ -286,6 +290,7 @@ class ColorPickerView @JvmOverloads constructor(
val rect = this.satValRect ?: return
val drawingRect = this.drawingRect ?: return
@Suppress("KotlinConstantConditions")
if (BORDER_WIDTH_PX > 0) {
borderPaint.color = borderColor
canvas.drawRect(
@ -366,9 +371,10 @@ class ColorPickerView @JvmOverloads constructor(
val p = satValToPoint(sat, bri)
satValTrackerPaint.color = -0x1000000
canvas.drawCircle(
p.x.toFloat(), p.y.toFloat(), (circleTrackerRadiusPx - DrawingUtils.dpToPx(
context, 1f
)).toFloat(), satValTrackerPaint
p.x.toFloat(),
p.y.toFloat(),
(circleTrackerRadiusPx - context.dpToPx(1f)).toFloat(),
satValTrackerPaint
)
satValTrackerPaint.color = -0x222223
canvas.drawCircle(
@ -381,6 +387,7 @@ class ColorPickerView @JvmOverloads constructor(
private fun drawHuePanel(canvas: Canvas) {
val rect = hueRect
@Suppress("KotlinConstantConditions")
if (BORDER_WIDTH_PX > 0) {
borderPaint.color = borderColor
canvas.drawRect(
@ -440,13 +447,14 @@ class ColorPickerView @JvmOverloads constructor(
private fun drawAlphaPanel(canvas: Canvas) {
if (!showAlphaPanel) return
val rect = this.alphaRect ?: return
val alphaPatternDrawable = this.alphaPatternDrawable ?: return
val alphaPatternDrawable = this.tilePatternDrawable ?: return
/*
* Will be drawn with hw acceleration, very fast.
* Also the AlphaPatternDrawable is backed by a bitmap
* generated only once if the size does not change.
*/
@Suppress("KotlinConstantConditions")
if (BORDER_WIDTH_PX > 0) {
borderPaint.color = borderColor
canvas.drawRect(
@ -477,9 +485,7 @@ class ColorPickerView @JvmOverloads constructor(
canvas.drawText(
it,
rect.centerX().toFloat(),
(rect.centerY() + DrawingUtils.dpToPx(
context, 4f
)).toFloat(),
(rect.centerY() + context.dpToPx(4f)).toFloat(),
alphaTextPaint
)
}
@ -579,6 +585,7 @@ class ColorPickerView @JvmOverloads constructor(
startTouchPoint = Point(event.x.toInt(), event.y.toInt())
update = moveTrackersIfNeeded(event)
}
MotionEvent.ACTION_MOVE -> update = moveTrackersIfNeeded(event)
MotionEvent.ACTION_UP -> {
startTouchPoint = null
@ -607,16 +614,19 @@ class ColorPickerView @JvmOverloads constructor(
hue = pointToHue(event.y)
true
}
satValRect?.contains(startX, startY) == true -> {
val result = pointToSatVal(event.x, event.y)
sat = result[0]
bri = result[1]
true
}
alphaRect?.contains(startX, startY) == true -> {
alpha = pointToAlpha(event.x.toInt())
true
}
else -> false
}
}
@ -679,14 +689,17 @@ class ColorPickerView @JvmOverloads constructor(
finalWidth = widthAllowed
finalHeight = heightNeeded
}
widthOk -> {
finalHeight = heightAllowed
finalWidth = widthNeeded
}
heightOk -> {
finalHeight = heightNeeded
finalWidth = widthAllowed
}
else -> {
finalHeight = heightAllowed
finalWidth = widthAllowed
@ -769,7 +782,7 @@ class ColorPickerView @JvmOverloads constructor(
val alphaRect = Rect(left, top, right, bottom)
.also { this.alphaRect = it }
alphaPatternDrawable = AlphaPatternDrawable(DrawingUtils.dpToPx(context, 4f))
tilePatternDrawable = TilePatternDrawable(context.dpToPx(4f))
.apply {
setBounds(
alphaRect.left,

View File

@ -1,30 +0,0 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jrummyapps.android.colorpicker
import androidx.annotation.IntDef
/**
* The shape of the color preview
*/
@Retention(AnnotationRetention.SOURCE)
@IntDef(ColorShape.SQUARE, ColorShape.CIRCLE)
annotation class ColorShape {
companion object {
const val SQUARE = 0
const val CIRCLE = 1
}
}

View File

@ -1,26 +0,0 @@
package com.jrummyapps.android.colorpicker
import android.graphics.Color
fun parseColorString(src: String): Int {
val start = if (src.startsWith("#")) 1 else 0
fun c1(offset: Int) =
src.substring(start + offset, start + offset + 1).toInt(16) * 0x11
fun c2(offset: Int) =
src.substring(start + offset, start + offset + 2).toInt(16)
return when (src.length - start) {
0 -> Color.BLACK
1 -> Color.argb(255, c1(0), c1(0), c1(0))
2 -> Color.argb(255, c1(0), c1(1), 0x80)
3 -> Color.argb(255, c1(0), c1(1), c1(2))
4 -> Color.argb(c1(0), c1(1), c1(2), c1(3))
5 -> Color.argb(255, c2(0), c2(2), c1(4))
6 -> Color.argb(255, c2(0), c2(2), c2(4))
7 -> Color.argb(c2(0), c2(2), c2(4), c1(6))
8 -> Color.argb(c2(0), c2(2), c2(4), c2(6))
else -> Color.WHITE
}
}

View File

@ -1,13 +0,0 @@
package com.jrummyapps.android.colorpicker
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String) =
if (Build.VERSION.SDK_INT >= 33) {
getParcelable(key, T::class.java)
} else {
@Suppress("DEPRECATION")
getParcelable(key)
}

View File

@ -1,29 +0,0 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jrummyapps.android.colorpicker
import android.content.Context
import android.util.TypedValue
internal object DrawingUtils {
fun dpToPx(c: Context, dipValue: Float): Int {
val metrics = c.resources.displayMetrics
val v = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, metrics)
val res = (v + 0.5).toInt() // Round
// Ensure at least 1 pixel if val was > 0
return if (res == 0 && v > 0) 1 else res
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jrummyapps.android.colorpicker
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable
import kotlin.math.min
/**
* This drawable will draw a simple white and gray chessboard pattern.
* It's the pattern you will often see as a background behind a partly transparent image in many applications.
*/
internal class TilePatternDrawable(
private val rectangleSize: Int,
) : Drawable() {
private val rect = Rect()
private val paintWhite = Paint().apply { color = Color.WHITE }
private val paintGray = Paint().apply { color = -0x343434 }
override fun draw(canvas: Canvas) {
val xEnd = bounds.right
val yEnd = bounds.bottom
var verticalStartWhite = true
for (y in bounds.top until yEnd step rectangleSize) {
rect.top = y
rect.bottom = min(yEnd, y + rectangleSize)
var isWhite = verticalStartWhite
for (x in bounds.left until xEnd step rectangleSize) {
rect.left = x
rect.right = min(xEnd, x + rectangleSize)
canvas.drawRect(rect, if (isWhite) paintWhite else paintGray)
isWhite = !isWhite
}
verticalStartWhite = !verticalStartWhite
}
}
@Suppress("OVERRIDE_DEPRECATION")
override fun getOpacity() = PixelFormat.OPAQUE
override fun setAlpha(alpha: Int) {
throw UnsupportedOperationException("Alpha is not supported by this drawable.")
}
override fun setColorFilter(cf: ColorFilter?) {
throw UnsupportedOperationException("ColorFilter is not supported by this drawable.")
}
}

View File

@ -35,4 +35,4 @@
<attr name="cpv_showDialog" format="boolean|reference"/>
</declare-styleable>
</resources>
</resources>

View File

@ -0,0 +1,219 @@
{
// json5
// ~/.gradle/caches フォルダ
"gradleCacheDir": "/c/Users/tateisu/.gradle/caches",
//デバッグ用。指定があればそのフォルダにpomファイルをコピーする。
// "pomDumpDir" : null,
// 不足データの取得に使うリポジトリ
"repos": [
"https://repo.maven.apache.org/maven2",
"https://dl.google.com/android/maven2",
"https://www.jitpack.io",
],
//////////////////////////////////////////////////////////////
// 出力ファイル別の設定
"outputs": [
{
"name": "fcm",
// 出力JSONファイルのpath
"outFile": "app/src/fcm/res/raw/dep_list.json",
// gradlew dependencies に指定するconfiguration
"configuration": "fcmReleaseRuntimeClasspath",
},
{
"name": "noFcm",
// 出力JSONファイルのpath
"outFile": "app/src/noFcm/res/raw/dep_list.json",
// gradlew dependencies に指定するconfiguration
"configuration": "noFcmReleaseRuntimeClasspath",
}
],
// Gradleの依存関係からは自動検出できない依存関係
"additionalLibs": [
{
"website": "https://github.com/iamcal/emoji-data",
"name": "iamcal/emoji-data",
"description": "Easy to parse data and spritesheets for emoji",
"licenses": [
{
"url": "https://opensource.org/license/mit/",
},
],
"developers": [
{
"name": "Cal Henderson"
}
],
},
{
"website": "https://github.com/twitter/twemoji",
"name": "Twitter Emoji (Twemoji)",
"description": "A simple library that provides standard Unicode emoji support across all platforms.",
"licenses": [
{
"url": "https://opensource.org/license/mit/",
},
],
"developers": [
{
"name": "X (fka Twitter)"
}
],
},
{
"website": "https://github.com/jrummyapps/colorpicker",
"name": "Color Picker",
"description": "Yet another open source color picker for Android.",
"licenses": [
{
"url": "https://www.apache.org/licenses/LICENSE-2.0",
},
],
"developers": [
{
"name": "Jared Rummler"
}
],
},
{
"website": "https://github.com/Kotlin/anko",
"name": "Kotlin/anko (Anko Layouts)",
"description": "a fast and type-safe way to write dynamic Android layouts",
"licenses": [
{
"url": "https://www.apache.org/licenses/LICENSE-2.0",
},
],
}
],
//////////////////////////////////////////////////////////////
// 既知のライセンス情報
// - URLの微妙な差異を吸収するため、あらかじめよくあるライセンスを列挙しておく
// - 実行時にPOMから取得した情報でデータが追加される
"licenses": [
{
"name": "The Apache Software License, Version 2.0",
"shortName": "Apache-2.0",
"urls": [
"https://www.apache.org/licenses/LICENSE-2.0",
"https://www.apache.org/licenses/LICENSE-2.0.txt",
"http://www.apache.org/licenses/LICENSE-2.0",
"http://www.apache.org/licenses/LICENSE-2.0.txt",
"https://api.github.com/licenses/apache-2.0",
"https://github.com/elye/loaderviewlibrary/blob/master/LICENSE",
],
},
{
"name": "MIT License",
"shortName": "MIT",
"urls": [
"https://opensource.org/license/mit/",
"https://opensource.org/licenses/MIT",
"https://github.com/lisawray/groupie/blob/master/LICENSE.md",
"https://github.com/omadahealth/SwipyRefreshLayoutblob/master/LICENSE",
],
},
{
"name": "The 2-Clause BSD License",
"shortName": "BSD-2-Clause",
"urls": [
"https://opensource.org/license/bsd-2-clause/",
"http://www.opensource.org/licenses/bsd-license",
],
},
/*
{
"name": "SQLCipher Community Edition License",
"shortName": "SQLCipher Community Edition License",
"urls": [
"https://www.zetetic.net/sqlcipher/license/",
],
},
*/
{
"name": "Amazon Software License",
"shortName": "Amazon Software License",
"urls": [
"https://aws.amazon.com/asl/",
"http://aws.amazon.com/asl/",
],
},
{
"name": "Unicode, Inc. License",
"shortName": "Unicode License",
"urls": [
"https://www.unicode.org/copyright.html#License",
"http://www.unicode.org/copyright.html#License",
],
},
],
// 以下のライブラリはpomにDevelopers指定がなくても許容する
"libsMissingDevelopers": [
"androidx.databinding:databinding-",
"androidx.databinding:databinding-adapters",
"androidx.databinding:viewbinding",
"com.amazonaws:aws-android-sdk-",
"com.github.alexzhirkevich:custom-qr-generator",
"com.github.penfeizhou.android.animation",
"com.google.android.datatransport:transport-",
"com.google.android.gms:play-services-",
"com.google.code.findbugs:jsr305:",
"com.google.code.gson:gson:",
"com.google.errorprone:error_prone_annotations:",
"com.google.firebase:firebase-",
"com.google.guava:failureaccess",
"com.google.guava:guava",
"com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava",
"com.google.zxing:core",
"com.jakewharton.picasso:picasso2-okhttp3-downloader",
"com.squareup.picasso:picasso",
"com.theartofdev.edmodo:android-image-cropper",
"io.realm:android-adapters",
"javax.inject:javax.inject",
"org.apache.httpcomponents:httpclient",
"org.apache.httpcomponents:httpcore",
"org.apache.httpcomponents:httpmime",
"org.eclipse.paho:org.eclipse.paho.client.mqttv3",
],
// 以下のライブラリはpomにライセンス指定がなくても許容する
"libsMissingLicenses": [
"com.github.alexzhirkevich:custom-qr-generator",
"com.github.penfeizhou.android.animation",
"com.google.guava:failureaccess",
"com.google.guava:guava",
"com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava",
"com.google.zxing:core",
"com.squareup.picasso:picasso",
"com.theartofdev.edmodo:android-image-cropper",
"commons-codec:commons-codec",
"commons-logging:commons-logging",
"org.apache.httpcomponents:httpclient",
"org.apache.httpcomponents:httpcore",
"org.apache.httpcomponents:httpmime",
"org.eclipse.paho:org.eclipse.paho.client.mqttv3",
],
// 以下のライブラリはpomにライセンス名の指定がなくても許容する
"libsMissingLicenseName": [
],
// 以下のライブラリはpomにWebサイト指定がなくても許容する
"libsMissingWebSite": [
"androidx.databinding:viewbinding",
"com.github.alexzhirkevich:custom-qr-generator",
"com.github.penfeizhou.android.animation",
"com.google.android.datatransport:transport-",
"com.google.android.gms:play-services-", // 前方一致
"com.google.errorprone:error_prone_annotations",
"com.google.firebase:firebase-", // 前方一致
"com.google.guava:failureaccess",
"com.google.guava:listenablefuture",
"com.google.zxing:core",
],
}

418
dependencyJson.pl Normal file
View File

@ -0,0 +1,418 @@
#!/usr/bin/perl --
# - カレントディレクトリで./gradlew :app:dependencies して依存関係を列挙する
# - ユーザフォルダの.gradle/ にあるpomファイルを探索する
# - 依存関係とpomファイルを突き合わせて json を出力する
use 5.32.1;
use strict;
use warnings;
use Getopt::Long;
use File::Basename;
use File::Find;
use File::Path qw(make_path remove_tree);
use File::Copy;
use JSON5;
use JSON::XS;
use Types::Serialiser;
use constant{
true =>Types::Serialiser::true,
false =>Types::Serialiser::false,
};
use XML::XPath;
use XML::XPath::XMLParser;
use Data::Dump qw(dump);
use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
use LWP::UserAgent;
my $ua = LWP::UserAgent->new(timeout => 10);
$ua->env_proxy;
sub loadFile($){
my($file)=@_;
open(my $fh,"<:raw",$file) or die "$! $file";
local $/ = undef;
my $data = <$fh>;
close($fh) or die "$! $file";
return $data;
}
# 出力フォルダがなければ作る
sub prepareDirectory($){
my($dir)=@_;
return if -d $dir;
make_path($dir) or die "can't create directory. $dir";
}
#####################################################
# オプション解析、値の検証、出力フォルダの作成
# 設定ファイル
my $configFile = "config/dependencyJsonConfig.json5";
GetOptions ("configFile=s" => \$configFile) or die("bad options.\n");
my $config = decode_json5(loadFile $configFile);
# ライブラリのライセンス情報
my $initialLicenses = $config->{licenses}
or die "config.initialLicenses is missing.";
# POM解析の検証用データ
# - POMのXML解析時に取得漏れがあればエラーとしたい
# - しかしXMLに元々情報がない場合はエラーを出したくない
# - なので情報がないライブラリを列挙しておく
# 以下のライブラリはpomにDevelopersがなくても許容する
my $libsMissingDevelopers = $config->{libsMissingDevelopers}
or die"config.libsMissingDevelopers is missing.";
# 以下のライブラリはpomにライセンス指定がなくても許容する
my $libsMissingLicenses = $config->{libsMissingLicenses}
or die"config.libsMissingLicenses is missing.";
# 以下のライブラリはpomにライセンス名の指定がなくても許容する
my $libsMissingLicenseName = $config->{libsMissingLicenseName}
or die"config.libsMissingLicenseName is missing.";
# 以下のライブラリはpomにWebサイトがなくても許容する
my $libsMissingWebSite = $config->{libsMissingWebSite}
or die"config.libsMissingWebSite is missing.";
# idがprefixesリストのいずれかに前方一致するなら真
sub matchLibs($$){
my($id,$prefixes)=@_;
for my $prefix(@$prefixes){
return true if $id =~/\A$prefix/;
}
return false;
}
# デバッグ用。指定があればそのフォルダにpomファイルをコピーする。
my $pomDir = $config->{pomDumpDir};
$pomDir and prepareDirectory( $pomDir );
# pomのメタ情報を読む
sub readPomInfo($$){
my($name, $xp)=@_;
my $groupId = $xp->findvalue('/project/groupId')->value()
|| $xp->findvalue('/project/parent/groupId')->value()
|| die "missing groupId in $name";
my $artifactId = $xp->findvalue('/project/artifactId')->value()
|| $xp->findvalue('/project/parent/artifactId')->value()
|| die "missing artifactId in $name";
my $version = $xp->findvalue('/project/version')->value()
|| $xp->findvalue('/project/parent/version')->value()
|| die "missing version in $name";
return {
groupId => $groupId,
artifactId => $artifactId,
version => $version,
#
fullName => "$groupId:$artifactId:$version",
groupAndArtifact => "$groupId:$artifactId",
};
}
# pomを読んで出力用データに変換する
sub parsePom($$){
my($errors,$found) = @_;
my $pomInfo = $found->{pomInfo};
my $id = $found->{dep};
# デバッグ用pomファイルをコピーする
# スクリプトから使う訳ではない
if($pomDir){
# idの:を_に変更する
my $idSafe = $id;
$idSafe =~ s/:/_/g;
# ファイルがまだなければコピーする
my $outPomFile = "$pomDir/$idSafe.pom";
-e $outPomFile or copy($pomInfo->{pomFile}, $outPomFile);
}
my $info = {
id => $id,
artifactVersion => $pomInfo->{version},
};
# xpathを使ってXMLからデータを読む
my $xp = XML::XPath->new(filename => $pomInfo->{pomFile});
my $developers = $info->{developers} = [];
for my $node( $xp->findnodes("/project/developers/developer") ){
my $name = $node->findvalue("name")->value()
|| $node->findvalue("id")->value();
if(not $name){
push @$errors,"[$id]missing developer.name";
next;
}
push @$developers,{ name => $name, };
}
if( not @$developers
and not matchLibs($id,$libsMissingDevelopers)
){
push @$errors,"[$id]missing developers.";
}
my $licenses = $info->{licenses} = [];
for my $node( $xp->findnodes("/project/licenses/license") ){
my $url = $node->findvalue('url')->value();
if(not $url){
push @$errors,"[$id]missing license.url";
next;
}
my $name = $node->findvalue('name')->value();
if( not $name){
if( matchLibs($id,$libsMissingLicenseName) ){
$name = "Unknown license";
}else{
push @$errors,"[$id]missing license.name";
next;
}
}
push @$licenses, { name => $name, url => $url, };
}
if( not @$licenses
and not matchLibs($id,$libsMissingLicenses)
){
push @$errors,"[$id]missing licenses.";
}
my $name = $xp->findvalue('/project/name')->value();
$name and $info->{name} = $name;
my $description = $xp->findvalue('/project/description')->value();
if($description){
$description =~ s/\A\s+//;
$description =~ s/\s+\z//;
$description and $info->{description} = $description;
}
my $webSite = $info->{website} = $xp->findvalue('/project/url')->value()
|| $xp->findvalue('/project/scm/url')->value();
if($webSite){
$info->{website} = $webSite;
}elsif( not matchLibs($id,$libsMissingWebSite) ){
push @$errors,"[$id]missing website.";
}
return $info;
}
# aarファイルにはpom.xmlが含まれないのでmvnコマンドで取得する。
sub downloadPom($){
my($dep)=@_;
# ダウンロードしたjarの保存フォルダ
my $dirDlSave = ".depCheck/download";
prepareDirectory( $dirDlSave );
# ダウンロードしたファイル
my $file = "$dirDlSave/$dep.pom";
$file =~ s/:/_/g;
if( not -f $file){
say "downloading pom for $dep";
$dep =~ m|^([^:]+):([^:]+):([^:]+)$|;
my($groupId,$artifactId,$version)=($1,$2,$3);
my $groupIdSlashed = $groupId;
$groupIdSlashed =~ s|\.|/|g;
my $successResponse;
my @errorResponses;
for my $repo(@{$config->{repos}}){
my $url = "$repo/$groupIdSlashed/$artifactId/$version/$artifactId-$version.pom";
my $response = $ua->get($url);
if( $response->is_success) {
$successResponse = $response;
last;
}else{
push @errorResponses,$response;
}
}
if(!$successResponse){
for(@errorResponses){
say $_->status_line ," ", $_->request->uri;
}
die "can't download $dep.";
}
open(my $fh,">:raw",$file) or die "$! $file";
print $fh $successResponse->content;
close($fh) or die "$! $file";
}
my $xp = XML::XPath->new(filename => $file);
my $pomInfo = readPomInfo($file,$xp);
$pomInfo->{pomFile} = $file;
return $pomInfo;
}
# gradleで依存関係を列挙する
sub listingDependencies($){
my($configuration)=@_;
my $cmd = "./gradlew -q --no-configuration-cache :app:dependencies --configuration $configuration";
say $cmd;
open(my $fh,"-|",$cmd) or die "failed to get dependencies: $!";
my %deps;
while(<$fh>){
s/[\x0d\x0a]+//;
s/\s+\z//;
# 依存関係は5文字単位でインデントされる
next if not s/\A[ \\|+-]{5,}//;
# 子プロジェクトは対象外
next if /^project :/;
# 末尾の注釈を除去
s/\s*\([c*]\)$//;
# "->" の対応:バージョンのみが変わる場合
s/([^ :]+?) -> ([^ :]+?)$/$2/;
# "->" の対応:パッケージごと変わる場合
s/(\S+?) -> (\S+?)$/$2/;
$_ and $deps{$_} = 1;
}
close($fh) or die "failed to get dependencies: $!";
my $depsCount = 0+(keys %deps);
$depsCount or die "ERROR: dependencies not found!";
say "$depsCount dependencies found.";
return \%deps;
}
# 依存関係とpomを照合してライブラリ毎の出力データを読み取る
sub mergeDepsAndPoms($){
my($depMap)=@_;
# 依存関係とpomを照合して @founds と @missings に分類する
my @founds;
for my $dep (sort keys %$depMap){
my $pomInfo = downloadPom($dep);
push @founds, {
dep => $dep,
pomInfo=>$pomInfo,
}
}
# pomのパース
my @errors;
my @info = map{ parsePom(\@errors, $_) } @founds;
if(@errors){
say $_ for @errors;
exit 1;
}
my $size = 0 + @info;
say "$size library information parsed.";
return \@info;
}
# @$licenses の要素でURLがマッチするものを返す
sub findLisenceByUrl($$){
my($licenses,$url) = @_;
for( @$licenses){
return $_ if grep{ $_ eq $url } @{$_->{urls}};
}
return;
}
# ライセンスのshortNameを返す
# @$licensesにデータがなければ追加する
sub licenseShortName($$){
my($licenses,$json)=@_;
my($item) = findLisenceByUrl($licenses,$json->{url});
if(not $item){
$item = {
shortName => $json->{name},
name => $json->{name},
urls =>[ $json->{url} ],
};
push @$licenses,$item;
}
return $item->{shortName};
}
# ライセンス情報をまとめる
sub compactLisences($$){
my($initialLicenseList,$libs)=@_;
# 変更するライセンスリスト
# ディープコピーする
my $licenses = decode_json encode_json $initialLicenseList;
# ライブラリごとにライセンスのリストがあるので、それをshortNameのリストに変換する
for my $lib (@$libs){
@{$lib->{licenses}} = map{ licenseShortName($licenses,$_) } @{$lib->{licenses}};
}
# 出力結果の並び順を安定させるため、ライセンス一覧をshortNameでソートする
@$licenses = sort {$a->{shortName} cmp $b->{shortName} } @$licenses;
say "licenses:";
for(@$licenses){
my $url = $_->{urls}[0];
say " [$_->{shortName}] name='$_->{name}' url=$url";
}
return $licenses;
}
# 情報をJSONファイルに出力
sub outputDepJson($$$){
my($outFile,$libs,$licences)=@_;
open(my $fh,">:raw",$outFile) or die "$outFile $!";
print $fh encode_json {
libs => $libs,
licenses => $licences,
};
close($fh) or die "$outFile $!";
}
##################################################
# - 出力ファイルごとの処理
# - ただしGradleキャッシュのスキャンは1回だけ
my $outputs = $config->{outputs} or die "contif.outputs is missing.";
@$outputs or die "contif.outputs is empty.";
# validation
my $outIndex = 0;
for my $out (@$outputs){
my $name = $out->{name} or die "config.outputs[$outIndex].name is missing.";
$out->{outFile} or die "config.outputs[$name].outFile is missing.";
$out->{configuration} or die "config.outputs[$name].configuration is missing.";
prepareDirectory( dirname($out->{outFile}) );
# gradleで依存関係を列挙する
say "# [$name] listing dependencies ...";
$out->{deps} = listingDependencies $out->{configuration};
# 依存関係とpomを照合してライブラリ毎の出力データを読み取る
say "# [$name] read lib data from dependencies and pom data.";
my $libs = mergeDepsAndPoms($out->{deps});
# 追加の依存関係
my $addItems = decode_json encode_json $config->{additionalLibs};
@$libs = ( @$addItems , @$libs );
# ライセンス情報をまとめる
say "# [$name] compacting licenses ...";
my $licenses = compactLisences($initialLicenses,$libs);
# 情報をJSONファイルに出力
say "# [$name] save to json $out->{outFile}";
outputDepJson($out->{outFile},$libs,$licenses);
++$outIndex;
}
say "complete!!";

View File

@ -68,4 +68,8 @@ dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:${Vers.desugarLibVersion}")
implementation(project(":base"))
implementation(project(":apng_android"))
implementation("androidx.appcompat:appcompat:${Vers.androidxAppcompat}")
// ないとなぜかIDE上にエラーが出る
implementation("androidx.activity:activity-ktx:${Vers.androidxActivity}")
}