runTestを使う。detektのチェック範囲を全モジュールとテストコードを含める。

This commit is contained in:
tateisu 2023-01-15 16:51:13 +09:00
parent b2b47e730a
commit 30b5beab64
65 changed files with 5548 additions and 5254 deletions

1
.gitignore vendored
View File

@ -133,3 +133,4 @@ detektReport/
app/detekt-*.xml app/detekt-*.xml
.idea/androidTestResultsUserPreferences.xml

View File

@ -5,33 +5,32 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence import jp.juggler.apng.util.ByteSequence
class ApngAnimationControl( class ApngAnimationControl(
// This must equal the number of `fcTL` chunks. // This must equal the number of `fcTL` chunks.
// 0 is not a valid value. // 0 is not a valid value.
// 1 is a valid value for a single-frame APNG. // 1 is a valid value for a single-frame APNG.
val numFrames : Int, val numFrames: Int,
// if it is 0, the animation should play indefinitely. // if it is 0, the animation should play indefinitely.
// If nonzero, the animation should come to rest on the final frame at the end of the last play. // If nonzero, the animation should come to rest on the final frame at the end of the last play.
val numPlays : Int = PLAY_INDEFINITELY val numPlays: Int = PLAY_INDEFINITELY,
) { ) {
companion object { companion object {
const val PLAY_INDEFINITELY = 0 const val PLAY_INDEFINITELY = 0
internal fun parse(src : ByteSequence) : ApngAnimationControl { internal fun parse(src: ByteSequence): ApngAnimationControl {
val numFrames = src.readInt32() val numFrames = src.readInt32()
val numPlays = src.readInt32() val numPlays = src.readInt32()
return ApngAnimationControl( return ApngAnimationControl(
numFrames = numFrames, numFrames = numFrames,
numPlays = numPlays numPlays = numPlays
) )
}
} }
}
override fun toString() = "ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"
override fun toString() = "ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"
val isFinite: Boolean
val isFinite : Boolean get() = numPlays > PLAY_INDEFINITELY
get() = numPlays > PLAY_INDEFINITELY
} }

View File

@ -4,36 +4,36 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence import jp.juggler.apng.util.ByteSequence
class ApngBackground internal constructor(colorType : ColorType, src : ByteSequence) { class ApngBackground internal constructor(colorType: ColorType, src: ByteSequence) {
val red : Int val red: Int
val green : Int val green: Int
val blue : Int val blue: Int
val index : Int val index: Int
init { init {
when(colorType) { when (colorType) {
ColorType.GREY, ColorType.GREY_ALPHA -> { ColorType.GREY, ColorType.GREY_ALPHA -> {
val v = src.readUInt16() val v = src.readUInt16()
red = v red = v
green = v green = v
blue = v blue = v
index = - 1 index = -1
} }
ColorType.RGB, ColorType.RGBA -> { ColorType.RGB, ColorType.RGBA -> {
red = src.readUInt16() red = src.readUInt16()
green = src.readUInt16() green = src.readUInt16()
blue = src.readUInt16() blue = src.readUInt16()
index = - 1 index = -1
} }
ColorType.INDEX -> { ColorType.INDEX -> {
red = - 1 red = -1
green = - 1 green = -1
blue = - 1 blue = -1
index = src.readUInt8() index = src.readUInt8()
} }
} }
} }
} }

View File

@ -2,63 +2,63 @@
package jp.juggler.apng package jp.juggler.apng
class ApngBitmap(var width : Int, var height : Int) { class ApngBitmap(var width: Int, var height: Int) {
// each int value contains 0xAARRGGBB // each int value contains 0xAARRGGBB
val colors = IntArray(width * height) val colors = IntArray(width * height)
// widthとheightを再指定する。ビットマップはそのまま再利用する // widthとheightを再指定する。ビットマップはそのまま再利用する
fun reset(width : Int, height : Int) { fun reset(width: Int, height: Int) {
val newSize = width * height val newSize = width * height
if(newSize > colors.size) if (newSize > colors.size)
throw ApngParseError("can't resize to $width x $height , it's greater than initial size") throw ApngParseError("can't resize to $width x $height , it's greater than initial size")
this.width = width this.width = width
this.height = height this.height = height
// 透明な黒で初期化する // 透明な黒で初期化する
colors.fill(0, fromIndex = 0, toIndex = newSize) colors.fill(0, fromIndex = 0, toIndex = newSize)
} }
// ビットマップ中の位置を保持して、ピクセルへの書き込みと位置の更新を行う // ビットマップ中の位置を保持して、ピクセルへの書き込みと位置の更新を行う
inner class Pointer { inner class Pointer {
private var pos : Int = 0 private var pos: Int = 0
var step : Int = 1 var step: Int = 1
fun setPixel(argb : Int) = apply { colors[pos] = argb } fun setPixel(argb: Int) = apply { colors[pos] = argb }
fun setPixel(a : Int, r : Int, g : Int, b : Int) = setPixel( fun setPixel(a: Int, r: Int, g: Int, b: Int) = setPixel(
((a and 255) shl 24) or ((a and 255) shl 24) or
((r and 255) shl 16) or ((r and 255) shl 16) or
((g and 255) shl 8) or ((g and 255) shl 8) or
(b and 255) (b and 255)
) )
fun setOffset(pos : Int = 0, step : Int = 1) = apply { fun setOffset(pos: Int = 0, step: Int = 1) = apply {
this.pos = pos this.pos = pos
this.step = step this.step = step
} }
fun setXY(x : Int, y : Int, step : Int = 1) = setOffset(x + y * width, step) fun setXY(x: Int, y: Int, step: Int = 1) = setOffset(x + y * width, step)
fun plus(x : Int) = apply { pos += x } fun plus(x: Int) = apply { pos += x }
fun next() = plus(step) fun next() = plus(step)
val color : Int val color: Int
get() = colors[pos] get() = colors[pos]
val alpha : Int val alpha: Int
get() = (colors[pos] shr 24) and 255 get() = (colors[pos] shr 24) and 255
val red : Int val red: Int
get() = (colors[pos] shr 16) and 255 get() = (colors[pos] shr 16) and 255
val green : Int val green: Int
get() = (colors[pos] shr 8) and 255 get() = (colors[pos] shr 8) and 255
val blue : Int val blue: Int
get() = (colors[pos]) and 255 get() = (colors[pos]) and 255
} }
fun pointer() = Pointer() fun pointer() = Pointer()
} }

View File

@ -5,35 +5,34 @@ package jp.juggler.apng
import jp.juggler.apng.util.StreamTokenizer import jp.juggler.apng.util.StreamTokenizer
import java.util.zip.CRC32 import java.util.zip.CRC32
internal class ApngChunk(crc32 : CRC32, tokenizer : StreamTokenizer) { internal class ApngChunk(crc32: CRC32, tokenizer: StreamTokenizer) {
val size : Int val size: Int
val type : String val type: String
init { init {
size = tokenizer.readInt32() size = tokenizer.readInt32()
val typeBytes = tokenizer.readBytes(4) val typeBytes = tokenizer.readBytes(4)
type = typeBytes.toString(Charsets.UTF_8) type = typeBytes.toString(Charsets.UTF_8)
crc32.update(typeBytes) crc32.update(typeBytes, 0, typeBytes.size)
} }
fun readBody(crc32 : CRC32, tokenizer : StreamTokenizer) : ByteArray { fun readBody(crc32: CRC32, tokenizer: StreamTokenizer): ByteArray {
val bytes = tokenizer.readBytes(size) val bytes = tokenizer.readBytes(size)
val crcExpect = tokenizer.readUInt32() val crcExpect = tokenizer.readUInt32()
crc32.update(bytes, 0, size) crc32.update(bytes, 0, size)
val crcActual = crc32.value val crcActual = crc32.value
if(crcActual != crcExpect) throw ApngParseError("CRC not match.") if (crcActual != crcExpect) throw ApngParseError("CRC not match.")
return bytes return bytes
} }
fun skipBody(tokenizer : StreamTokenizer) = fun skipBody(tokenizer: StreamTokenizer) =
tokenizer.skipBytes((size + 4).toLong()) tokenizer.skipBytes((size + 4).toLong())
fun checkCRC(tokenizer: StreamTokenizer, crcActual: Long) {
fun checkCRC(tokenizer : StreamTokenizer, crcActual : Long) { val crcExpect = tokenizer.readUInt32()
val crcExpect = tokenizer.readUInt32() if (crcActual != crcExpect) throw ApngParseError("CRC not match.")
if(crcActual != crcExpect) throw ApngParseError("CRC not match.") }
}
} }

View File

@ -7,165 +7,165 @@ import java.io.InputStream
import java.util.zip.CRC32 import java.util.zip.CRC32
object ApngDecoder { object ApngDecoder {
private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a) private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a)
fun parseStream( fun parseStream(
inStream : InputStream, inStream: InputStream,
callback : ApngDecoderCallback callback: ApngDecoderCallback,
) { ) {
val apng = Apng() val apng = Apng()
val tokenizer = StreamTokenizer(inStream) val tokenizer = StreamTokenizer(inStream)
val pngHeader = tokenizer.readBytes(8) val pngHeader = tokenizer.readBytes(8)
if(! pngHeader.contentEquals(PNG_SIGNATURE)) { if (!pngHeader.contentEquals(PNG_SIGNATURE)) {
throw ApngParseError("header not match") throw ApngParseError("header not match")
} }
var lastSequenceNumber : Int? = null var lastSequenceNumber: Int? = null
fun checkSequenceNumber(n : Int) { fun checkSequenceNumber(n: Int) {
val last = lastSequenceNumber val last = lastSequenceNumber
if(last != null && n <= last) { if (last != null && n <= last) {
throw ApngParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n") throw ApngParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n")
} }
lastSequenceNumber = n lastSequenceNumber = n
} }
val inBuffer = ByteArray(4096) val inBuffer = ByteArray(4096)
val inflateBufferPool = BufferPool(8192) val inflateBufferPool = BufferPool(8192)
var idatDecoder : IdatDecoder? = null var idatDecoder: IdatDecoder? = null
var fdatDecoder : IdatDecoder? = null var fdatDecoder: IdatDecoder? = null
val crc32 = CRC32() val crc32 = CRC32()
var lastFctl : ApngFrameControl? = null var lastFctl: ApngFrameControl? = null
var bitmap : ApngBitmap? = null var bitmap: ApngBitmap? = null
loop@ while(true) { loop@ while (true) {
crc32.reset() crc32.reset()
val chunk = ApngChunk(crc32, tokenizer) val chunk = ApngChunk(crc32, tokenizer)
when(chunk.type) { when (chunk.type) {
"IEND" -> break@loop "IEND" -> break@loop
"IHDR" -> { "IHDR" -> {
val header = val header =
ApngImageHeader.parse(ByteSequence(chunk.readBody(crc32, tokenizer))) ApngImageHeader.parse(ByteSequence(chunk.readBody(crc32, tokenizer)))
bitmap = ApngBitmap(header.width, header.height) bitmap = ApngBitmap(header.width, header.height)
apng.header = header apng.header = header
callback.onHeader(apng, header) callback.onHeader(apng, header)
} }
"PLTE" -> apng.palette = ApngPalette(chunk.readBody(crc32, tokenizer)) "PLTE" -> apng.palette = ApngPalette(chunk.readBody(crc32, tokenizer))
"bKGD" -> { "bKGD" -> {
val header = apng.header ?: throw ApngParseError("missing IHDR") val header = apng.header ?: throw ApngParseError("missing IHDR")
apng.background = ApngBackground( apng.background = ApngBackground(
header.colorType, header.colorType,
ByteSequence(chunk.readBody(crc32, tokenizer)) ByteSequence(chunk.readBody(crc32, tokenizer))
) )
} }
"tRNS" -> { "tRNS" -> {
val header = apng.header ?: throw ApngParseError("missing IHDR") val header = apng.header ?: throw ApngParseError("missing IHDR")
val body = chunk.readBody(crc32, tokenizer) val body = chunk.readBody(crc32, tokenizer)
when(header.colorType) { when (header.colorType) {
ColorType.GREY -> apng.transparentColor = ColorType.GREY -> apng.transparentColor =
ApngTransparentColor(true, ByteSequence(body)) ApngTransparentColor(true, ByteSequence(body))
ColorType.RGB -> apng.transparentColor = ColorType.RGB -> apng.transparentColor =
ApngTransparentColor(false, ByteSequence(body)) ApngTransparentColor(false, ByteSequence(body))
ColorType.INDEX -> apng.palette?.parseTRNS(body) ColorType.INDEX -> apng.palette?.parseTRNS(body)
?: throw ApngParseError("missing palette") ?: throw ApngParseError("missing palette")
else -> callback.onApngWarning("tRNS ignored. colorType =${header.colorType}") else -> callback.onApngWarning("tRNS ignored. colorType =${header.colorType}")
} }
} }
"IDAT" -> { "IDAT" -> {
val header = apng.header ?: throw ApngParseError("missing IHDR") val header = apng.header ?: throw ApngParseError("missing IHDR")
if(idatDecoder == null) { if (idatDecoder == null) {
bitmap ?: throw ApngParseError("missing bitmap") bitmap ?: throw ApngParseError("missing bitmap")
bitmap.reset(header.width, header.height) bitmap.reset(header.width, header.height)
idatDecoder = IdatDecoder( idatDecoder = IdatDecoder(
apng, apng,
bitmap, bitmap,
inflateBufferPool, inflateBufferPool,
callback callback
) { ) {
callback.onDefaultImage(apng, bitmap) callback.onDefaultImage(apng, bitmap)
val fctl = lastFctl val fctl = lastFctl
if(fctl != null) { if (fctl != null) {
// IDATより前にfcTLが登場しているなら、そのfcTLの画像はIDATと同じ // IDATより前にfcTLが登場しているなら、そのfcTLの画像はIDATと同じ
callback.onAnimationFrame(apng, fctl, bitmap) callback.onAnimationFrame(apng, fctl, bitmap)
} }
} }
} }
idatDecoder.addData( idatDecoder.addData(
tokenizer.inStream, tokenizer.inStream,
chunk.size, chunk.size,
inBuffer, inBuffer,
crc32 crc32
) )
chunk.checkCRC(tokenizer, crc32.value) chunk.checkCRC(tokenizer, crc32.value)
} }
"acTL" -> { "acTL" -> {
val header = apng.header ?: throw ApngParseError("missing IHDR") val header = apng.header ?: throw ApngParseError("missing IHDR")
val animationControl = ApngAnimationControl val animationControl = ApngAnimationControl
.parse(ByteSequence(chunk.readBody(crc32, tokenizer))) .parse(ByteSequence(chunk.readBody(crc32, tokenizer)))
apng.animationControl = animationControl apng.animationControl = animationControl
callback.onAnimationInfo(apng, header, animationControl) callback.onAnimationInfo(apng, header, animationControl)
} }
"fcTL" -> { "fcTL" -> {
val bat = ByteSequence(chunk.readBody(crc32, tokenizer)) val bat = ByteSequence(chunk.readBody(crc32, tokenizer))
val sequenceNumber = bat.readInt32() val sequenceNumber = bat.readInt32()
checkSequenceNumber(sequenceNumber) checkSequenceNumber(sequenceNumber)
lastFctl = ApngFrameControl.parse(bat, sequenceNumber) lastFctl = ApngFrameControl.parse(bat, sequenceNumber)
fdatDecoder = null fdatDecoder = null
} }
"fdAT" -> { "fdAT" -> {
val fctl = lastFctl ?: throw ApngParseError("missing fCTL before fdAT") val fctl = lastFctl ?: throw ApngParseError("missing fCTL before fdAT")
if(fdatDecoder == null) { if (fdatDecoder == null) {
bitmap ?: throw ApngParseError("missing bitmap") bitmap ?: throw ApngParseError("missing bitmap")
bitmap.reset(fctl.width, fctl.height) bitmap.reset(fctl.width, fctl.height)
fdatDecoder = IdatDecoder( fdatDecoder = IdatDecoder(
apng, apng,
bitmap, bitmap,
inflateBufferPool, inflateBufferPool,
callback callback
) { ) {
callback.onAnimationFrame(apng, fctl, bitmap) callback.onAnimationFrame(apng, fctl, bitmap)
} }
} }
val sequenceNumber = tokenizer.readInt32(crc32) val sequenceNumber = tokenizer.readInt32(crc32)
checkSequenceNumber(sequenceNumber) checkSequenceNumber(sequenceNumber)
fdatDecoder.addData( fdatDecoder.addData(
tokenizer.inStream, tokenizer.inStream,
chunk.size - 4, chunk.size - 4,
inBuffer, inBuffer,
crc32 crc32
) )
chunk.checkCRC(tokenizer, crc32.value) chunk.checkCRC(tokenizer, crc32.value)
} }
// 無視するチャンク // 無視するチャンク
"cHRM", "gAMA", "iCCP", "sBIT", "sRGB", // color space information "cHRM", "gAMA", "iCCP", "sBIT", "sRGB", // color space information
"tEXt", "zTXt", "iTXt", // text information "tEXt", "zTXt", "iTXt", // text information
"tIME", // timestamp "tIME", // timestamp
"hIST", // histogram "hIST", // histogram
"pHYs", // Physical pixel dimensions "pHYs", // Physical pixel dimensions
"sPLT" // Suggested palette (おそらく減色用?) "sPLT", // Suggested palette (おそらく減色用?)
-> chunk.skipBody(tokenizer) -> chunk.skipBody(tokenizer)
else -> { else -> {
callback.onApngWarning( callback.onApngWarning(
"unknown chunk: type=%s,size=0x%x".format( "unknown chunk: type=%s,size=0x%x".format(
chunk.type, chunk.type,
chunk.size chunk.size
) )
) )
chunk.skipBody(tokenizer) chunk.skipBody(tokenizer)
} }
} }
} }
} }
} }

View File

@ -1,31 +1,30 @@
package jp.juggler.apng package jp.juggler.apng
interface ApngDecoderCallback { interface ApngDecoderCallback {
// called for non-fatal warning // called for non-fatal warning
fun onApngWarning(message : String) fun onApngWarning(message: String)
// called for debug message // called for debug message
fun onApngDebug(message : String) {} fun onApngDebug(message: String) {}
fun canApngDebug() : Boolean = false fun canApngDebug(): Boolean = false
// called when PNG image header is detected. // called when PNG image header is detected.
fun onHeader(apng : Apng, header : ApngImageHeader) fun onHeader(apng: Apng, header: ApngImageHeader)
// called when APNG Animation Control is detected. // called when APNG Animation Control is detected.
fun onAnimationInfo( fun onAnimationInfo(
apng : Apng, apng: Apng,
header : ApngImageHeader, header: ApngImageHeader,
animationControl : ApngAnimationControl animationControl: ApngAnimationControl,
) )
// called when default image bitmap was rendered. // called when default image bitmap was rendered.
fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) fun onDefaultImage(apng: Apng, bitmap: ApngBitmap)
// called when APNG Frame Control is detected and its bitmap was rendered. // called when APNG Frame Control is detected and its bitmap was rendered.
// its bitmap may same to default image for first frame. // its bitmap may same to default image for first frame.
// ( in this case, both of onDefaultImage and onAnimationFrame are called for same bitmap) // ( in this case, both of onDefaultImage and onAnimationFrame are called for same bitmap)
fun onAnimationFrame(apng : Apng, frameControl : ApngFrameControl, frameBitmap : ApngBitmap) fun onAnimationFrame(apng: Apng, frameControl: ApngFrameControl, frameBitmap: ApngBitmap)
} }

View File

@ -4,51 +4,50 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence import jp.juggler.apng.util.ByteSequence
class ApngFrameControl ( class ApngFrameControl(
val width : Int, val width: Int,
val height : Int, val height: Int,
val xOffset : Int, val xOffset: Int,
val yOffset : Int, val yOffset: Int,
val disposeOp : DisposeOp, val disposeOp: DisposeOp,
val blendOp : BlendOp, val blendOp: BlendOp,
val sequenceNumber:Int, val sequenceNumber: Int,
val delayMilliseconds: Long val delayMilliseconds: Long,
) { ) {
companion object{
internal fun parse(src : ByteSequence, sequenceNumber:Int) :ApngFrameControl{
val width = src.readInt32()
val height = src.readInt32()
val xOffset = src.readInt32()
val yOffset = src.readInt32()
val delayNum = src.readUInt16()
val delayDen = src.readUInt16().let { if(it == 0) 100 else it }
var num : Int
num = src.readUInt8()
val disposeOp = DisposeOp.values().first { it.num == num }
num = src.readUInt8()
val blendOp = BlendOp.values().first { it.num == num }
return ApngFrameControl( companion object {
width =width, internal fun parse(src: ByteSequence, sequenceNumber: Int): ApngFrameControl {
height = height, val width = src.readInt32()
xOffset = xOffset, val height = src.readInt32()
yOffset = yOffset, val xOffset = src.readInt32()
disposeOp = disposeOp, val yOffset = src.readInt32()
blendOp = blendOp, val delayNum = src.readUInt16()
sequenceNumber = sequenceNumber, val delayDen = src.readUInt16().let { if (it == 0) 100 else it }
delayMilliseconds = when(delayDen) {
0,1000 -> delayNum.toLong()
else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong()
}
)
}
}
override fun toString() =
"ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayMilliseconds=$delayMilliseconds,disposeOp=$disposeOp,blendOp=$blendOp)"
var num: Int
num = src.readUInt8()
val disposeOp = DisposeOp.values().first { it.num == num }
num = src.readUInt8()
val blendOp = BlendOp.values().first { it.num == num }
return ApngFrameControl(
width = width,
height = height,
xOffset = xOffset,
yOffset = yOffset,
disposeOp = disposeOp,
blendOp = blendOp,
sequenceNumber = sequenceNumber,
delayMilliseconds = when (delayDen) {
0, 1000 -> delayNum.toLong()
else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong()
}
)
}
}
override fun toString() =
"ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayMilliseconds=$delayMilliseconds,disposeOp=$disposeOp,blendOp=$blendOp)"
} }

View File

@ -6,50 +6,48 @@ import jp.juggler.apng.util.ByteSequence
// information from IHDR chunk. // information from IHDR chunk.
class ApngImageHeader( class ApngImageHeader(
val width : Int, val width: Int,
val height : Int, val height: Int,
val bitDepth : Int, val bitDepth: Int,
val colorType : ColorType, val colorType: ColorType,
val compressionMethod : CompressionMethod, val compressionMethod: CompressionMethod,
val filterMethod : FilterMethod, val filterMethod: FilterMethod,
val interlaceMethod : InterlaceMethod val interlaceMethod: InterlaceMethod,
) { ) {
companion object{ companion object {
internal fun parse (src : ByteSequence) :ApngImageHeader{ internal fun parse(src: ByteSequence): ApngImageHeader {
val width = src.readInt32()
val width = src.readInt32() val height = src.readInt32()
val height = src.readInt32() if (width <= 0 || height <= 0) throw ApngParseError("w=$width,h=$height is too small")
if(width <= 0 || height <= 0) throw ApngParseError("w=$width,h=$height is too small")
val bitDepth = src.readUInt8()
var num : Int
//
num = src.readUInt8()
val colorType = ColorType.values().first { it.num == num }
//
num = src.readUInt8()
val compressionMethod = CompressionMethod.values().first { it.num == num }
//
num = src.readUInt8()
val filterMethod = FilterMethod.values().first { it.num == num }
//
num = src.readUInt8()
val interlaceMethod = InterlaceMethod.values().first { it.num == num }
return ApngImageHeader( val bitDepth = src.readUInt8()
width =width,
height = height, var num: Int
bitDepth = bitDepth, //
colorType = colorType, num = src.readUInt8()
compressionMethod = compressionMethod, val colorType = ColorType.values().first { it.num == num }
filterMethod = filterMethod, //
interlaceMethod = interlaceMethod num = src.readUInt8()
val compressionMethod = CompressionMethod.values().first { it.num == num }
) //
} num = src.readUInt8()
} val filterMethod = FilterMethod.values().first { it.num == num }
//
override fun toString() = num = src.readUInt8()
"ApngImageHeader(w=$width,h=$height,bits=$bitDepth,color=$colorType,interlace=$interlaceMethod)" val interlaceMethod = InterlaceMethod.values().first { it.num == num }
return ApngImageHeader(
width = width,
height = height,
bitDepth = bitDepth,
colorType = colorType,
compressionMethod = compressionMethod,
filterMethod = filterMethod,
interlaceMethod = interlaceMethod,
)
}
}
override fun toString() =
"ApngImageHeader(w=$width,h=$height,bits=$bitDepth,color=$colorType,interlace=$interlaceMethod)"
} }

View File

@ -6,38 +6,38 @@ import jp.juggler.apng.util.getUInt8
import kotlin.math.min import kotlin.math.min
class ApngPalette( class ApngPalette(
src : ByteArray // repeat of R,G,B src: ByteArray, // repeat of R,G,B
) { ) {
companion object { companion object {
// full opaque black // full opaque black
const val OPAQUE = 255 shl 24 const val OPAQUE = 255 shl 24
} }
val list : IntArray // repeat of 0xAARRGGBB val list: IntArray // repeat of 0xAARRGGBB
var hasAlpha : Boolean = false var hasAlpha: Boolean = false
init { init {
val entryCount = src.size / 3 val entryCount = src.size / 3
list = IntArray(entryCount) list = IntArray(entryCount)
var pos = 0 var pos = 0
for(i in 0 until entryCount) { for (i in 0 until entryCount) {
list[i] = OPAQUE or list[i] = OPAQUE or
(src.getUInt8(pos) shl 16) or (src.getUInt8(pos) shl 16) or
(src.getUInt8(pos + 1) shl 8) or (src.getUInt8(pos + 1) shl 8) or
src.getUInt8(pos + 2) src.getUInt8(pos + 2)
pos += 3 pos += 3
} }
} }
override fun toString() = "palette(${list.size} entries,hasAlpha=$hasAlpha)" override fun toString() = "palette(${list.size} entries,hasAlpha=$hasAlpha)"
// update alpha value from tRNS chunk data // update alpha value from tRNS chunk data
fun parseTRNS(ba : ByteArray) { fun parseTRNS(ba: ByteArray) {
hasAlpha = true hasAlpha = true
for(i in 0 until min(list.size, ba.size)) { for (i in 0 until min(list.size, ba.size)) {
list[i] = (list[i] and 0xffffff) or (ba.getUInt8(i) shl 24) list[i] = (list[i] and 0xffffff) or (ba.getUInt8(i) shl 24)
} }
} }
} }

View File

@ -4,24 +4,24 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence import jp.juggler.apng.util.ByteSequence
class ApngTransparentColor internal constructor(isGreyScale : Boolean, src : ByteSequence) { class ApngTransparentColor internal constructor(isGreyScale: Boolean, src: ByteSequence) {
val red : Int val red: Int
val green : Int val green: Int
val blue : Int val blue: Int
init { init {
if(isGreyScale) { if (isGreyScale) {
val v = src.readUInt16() val v = src.readUInt16()
red = v red = v
green = v green = v
blue = v blue = v
} else { } else {
red = src.readUInt16() red = src.readUInt16()
green = src.readUInt16() green = src.readUInt16()
blue = src.readUInt16() blue = src.readUInt16()
} }
} }
fun match(grey : Int) = red == grey fun match(grey: Int) = red == grey
fun match(r : Int, g : Int, b : Int) = (r == red && g == green && b == blue) fun match(r: Int, g: Int, b: Int) = (r == red && g == green && b == blue)
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,10 @@
package jp.juggler.apng package jp.juggler.apng
interface GifDecoderCallback { interface GifDecoderCallback {
fun onGifWarning(message : String) fun onGifWarning(message: String)
fun onGifDebug(message : String) fun onGifDebug(message: String)
fun canGifDebug() : Boolean fun canGifDebug(): Boolean
fun onGifHeader(header : ApngImageHeader) fun onGifHeader(header: ApngImageHeader)
fun onGifAnimationInfo( header : ApngImageHeader, animationControl : ApngAnimationControl ) fun onGifAnimationInfo(header: ApngImageHeader, animationControl: ApngAnimationControl)
fun onGifAnimationFrame( frameControl : ApngFrameControl, frameBitmap : ApngBitmap ) fun onGifAnimationFrame(frameControl: ApngFrameControl, frameBitmap: ApngBitmap)
} }

View File

@ -14,7 +14,7 @@ internal class IdatDecoder(
private val bitmap: ApngBitmap, private val bitmap: ApngBitmap,
private val inflateBufferPool: BufferPool, private val inflateBufferPool: BufferPool,
private val callback: ApngDecoderCallback, private val callback: ApngDecoderCallback,
private val onCompleted: () -> Unit private val onCompleted: () -> Unit,
) { ) {
private class PassInfo(val xStep: Int, val xStart: Int, val yStep: Int, val yStart: Int) private class PassInfo(val xStep: Int, val xStart: Int, val yStep: Int, val yStart: Int)
@ -47,9 +47,9 @@ internal class IdatDecoder(
} }
} }
private inline fun scanLine1(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) { private inline fun scanLine1(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) {
var pos = 1 var pos = 1
var remain = pass_w var remain = passW
while (remain >= 8) { while (remain >= 8) {
remain -= 8 remain -= 8
val v = baLine[pos++].toInt() val v = baLine[pos++].toInt()
@ -74,9 +74,9 @@ internal class IdatDecoder(
} }
} }
private inline fun scanLine2(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) { private inline fun scanLine2(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) {
var pos = 1 var pos = 1
var remain = pass_w var remain = passW
while (remain >= 4) { while (remain >= 4) {
remain -= 4 remain -= 4
val v = baLine[pos++].toInt() val v = baLine[pos++].toInt()
@ -93,9 +93,9 @@ internal class IdatDecoder(
} }
} }
private inline fun scanLine4(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) { private inline fun scanLine4(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) {
var pos = 1 var pos = 1
var remain = pass_w var remain = passW
while (remain >= 2) { while (remain >= 2) {
remain -= 2 remain -= 2
val v = baLine[pos++].toInt() val v = baLine[pos++].toInt()
@ -108,14 +108,13 @@ internal class IdatDecoder(
} }
} }
private inline fun scanLine8(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) { private inline fun scanLine8(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) {
var pos = 1 var pos = 1
var remain = pass_w var remain = passW
while (remain-- > 0) { while (remain-- > 0) {
block(baLine.getUInt8(pos++)) block(baLine.getUInt8(pos++))
} }
} }
} }
private val inflater = Inflater() private val inflater = Inflater()
@ -405,7 +404,7 @@ internal class IdatDecoder(
val filterNum = baLine.getUInt8(0) val filterNum = baLine.getUInt8(0)
// if( callback.canApngDebug() ) callback.onApngDebug("y=$passY/${passHeight},filterType=$filterType") // if( callback.canApngDebug() ) callback.onApngDebug("y=$passY/${passHeight},filterType=$filterType")
when (FilterType.values().first { it.num == filterNum }) { when (FilterType.values().first { it.num == filterNum }) {
FilterType.None -> { FilterType.None -> {
@ -424,7 +423,6 @@ internal class IdatDecoder(
// val y = passInfo.yStart + passInfo.yStep * passY // val y = passInfo.yStart + passInfo.yStep * passY
// callback.onApngDebug("sub pos=$pos,x=$x,y=$y,left=$vLeft,cur=$vCur,after=${baLine[pos].toInt() and 255}") // callback.onApngDebug("sub pos=$pos,x=$x,y=$y,left=$vLeft,cur=$vCur,after=${baLine[pos].toInt() and 255}")
// } // }
} }
} }
@ -464,7 +462,6 @@ internal class IdatDecoder(
// val y = passInfo.yStart + passInfo.yStep * passY // val y = passInfo.yStart + passInfo.yStep * passY
// callback.onApngDebug("paeth pos=$pos,x=$x,y=$y,left=$vLeft,up=$vUp,ul=$vUpperLeft,cur=$vCur,paeth=${paeth(vLeft, vUp, vUpperLeft)}") // callback.onApngDebug("paeth pos=$pos,x=$x,y=$y,left=$vLeft,up=$vUp,ul=$vUpperLeft,cur=$vCur,paeth=${paeth(vLeft, vUp, vUpperLeft)}")
// } // }
} }
} }
} }
@ -498,7 +495,7 @@ internal class IdatDecoder(
inStream: InputStream, inStream: InputStream,
size: Int, size: Int,
inBuffer: ByteArray, inBuffer: ByteArray,
crc32: CRC32 crc32: CRC32,
) { ) {
var foundEnd = false var foundEnd = false
var inRemain = size var inRemain = size
@ -541,11 +538,11 @@ internal class IdatDecoder(
inflateBufferQueue.add(ByteSequence(buffer, 0, nInflated)) inflateBufferQueue.add(ByteSequence(buffer, 0, nInflated))
// キューに追加したデータをScanLine単位で消費する // キューに追加したデータをScanLine単位で消費する
@Suppress("ControlFlowWithEmptyBody") @Suppress("ControlFlowWithEmptyBody", "EmptyWhileBlock")
while (!isCompleted && readScanLine()){ while (!isCompleted && readScanLine()) {
} }
if (isCompleted) { if (isCompleted) {
inflateBufferQueue.clear() inflateBufferQueue.clear()
break break
} }

View File

@ -2,8 +2,8 @@ package jp.juggler.apng.util
import java.util.* import java.util.*
internal class BufferPool(private val arraySize : Int) { internal class BufferPool(private val arraySize: Int) {
private val list = LinkedList<ByteArray>() private val list = LinkedList<ByteArray>()
fun obtain() : ByteArray = if(list.isEmpty()) ByteArray(arraySize) else list.removeFirst() fun obtain(): ByteArray = if (list.isEmpty()) ByteArray(arraySize) else list.removeFirst()
fun recycle(array : ByteArray?) = array?.let { list.add(it) } fun recycle(array: ByteArray?) = array?.let { list.add(it) }
} }

View File

@ -2,32 +2,32 @@ package jp.juggler.apng.util
import jp.juggler.apng.ApngParseError import jp.juggler.apng.ApngParseError
internal fun ByteArray.getUInt8(pos : Int) = get(pos).toInt() and 255 internal fun ByteArray.getUInt8(pos: Int) = get(pos).toInt() and 255
internal fun ByteArray.getUInt16(pos : Int) = (getUInt8(pos) shl 8) or getUInt8(pos + 1) internal fun ByteArray.getUInt16(pos: Int) = (getUInt8(pos) shl 8) or getUInt8(pos + 1)
internal fun ByteArray.getInt32(pos : Int) = (getUInt8(pos) shl 24) or internal fun ByteArray.getInt32(pos: Int) = (getUInt8(pos) shl 24) or
(getUInt8(pos + 1) shl 16) or (getUInt8(pos + 1) shl 16) or
(getUInt8(pos + 2) shl 8) or (getUInt8(pos + 2) shl 8) or
getUInt8(pos + 3) getUInt8(pos + 3)
internal class ByteSequence( internal class ByteSequence(
val array : ByteArray, val array: ByteArray,
var offset : Int, var offset: Int,
var length : Int var length: Int,
) { ) {
constructor(ba : ByteArray) : this(ba, 0, ba.size) constructor(ba: ByteArray) : this(ba, 0, ba.size)
private inline fun <T> readX(dataSize : Int, block : () -> T) : T { private inline fun <T> readX(dataSize: Int, block: () -> T): T {
if(length < dataSize) throw ApngParseError("readX: unexpected end") if (length < dataSize) throw ApngParseError("readX: unexpected end")
val v = block() val v = block()
offset += dataSize offset += dataSize
length -= dataSize length -= dataSize
return v return v
} }
fun readUInt8() = readX(1) { array.getUInt8(offset) } fun readUInt8() = readX(1) { array.getUInt8(offset) }
fun readUInt16() = readX(2) { array.getUInt16(offset) } fun readUInt16() = readX(2) { array.getUInt16(offset) }
fun readInt32() = readX(4) { array.getInt32(offset) } fun readInt32() = readX(4) { array.getInt32(offset) }
} }

View File

@ -3,34 +3,34 @@ package jp.juggler.apng.util
import java.util.* import java.util.*
import kotlin.math.min import kotlin.math.min
internal class ByteSequenceQueue(private val bufferRecycler : (ByteSequence) -> Unit) { internal class ByteSequenceQueue(private val bufferRecycler: (ByteSequence) -> Unit) {
private val list = LinkedList<ByteSequence>() private val list = LinkedList<ByteSequence>()
val remain : Int val remain: Int
get() = list.sumOf { it.length } get() = list.sumOf { it.length }
fun add(range : ByteSequence) =list.add(range) fun add(range: ByteSequence) = list.add(range)
fun clear() = list.onEach(bufferRecycler).clear() fun clear() = list.onEach(bufferRecycler).clear()
fun readBytes(dst : ByteArray, offset : Int, length : Int) : Int { fun readBytes(dst: ByteArray, offset: Int, length: Int): Int {
var dstOffset = offset var dstOffset = offset
var dstRemain = length var dstRemain = length
while(dstRemain > 0 && list.isNotEmpty()) { while (dstRemain > 0 && list.isNotEmpty()) {
val item = list.first() val item = list.first()
if(item.length <= 0) { if (item.length <= 0) {
bufferRecycler(item) bufferRecycler(item)
list.removeFirst() list.removeFirst()
} else { } else {
val delta = min(item.length, dstRemain) val delta = min(item.length, dstRemain)
System.arraycopy(item.array, item.offset, dst, dstOffset, delta) System.arraycopy(item.array, item.offset, dst, dstOffset, delta)
dstOffset += delta dstOffset += delta
dstRemain -= delta dstRemain -= delta
item.offset += delta item.offset += delta
item.length -= delta item.length -= delta
} }
} }
return length - dstRemain return length - dstRemain
} }
} }

View File

@ -4,63 +4,63 @@ import jp.juggler.apng.ApngParseError
import java.io.InputStream import java.io.InputStream
import java.util.zip.CRC32 import java.util.zip.CRC32
internal class StreamTokenizer(val inStream : InputStream) { internal class StreamTokenizer(val inStream: InputStream) {
fun skipBytes(size : Long) { fun skipBytes(size: Long) {
var nRead = 0L var nRead = 0L
while(true) { while (true) {
val remain = size - nRead val remain = size - nRead
if(remain <= 0) break if (remain <= 0) break
val delta = inStream.skip(size - nRead) val delta = inStream.skip(size - nRead)
if(delta <= 0) throw ApngParseError("skipBytes: unexpected EoS") if (delta <= 0) throw ApngParseError("skipBytes: unexpected EoS")
nRead += delta nRead += delta
} }
} }
fun readBytes(size : Int) : ByteArray { fun readBytes(size: Int): ByteArray {
val dst = ByteArray(size) val dst = ByteArray(size)
var nRead = 0 var nRead = 0
while(true) { while (true) {
val remain = size - nRead val remain = size - nRead
if(remain <= 0) break if (remain <= 0) break
val delta = inStream.read(dst, nRead, size - nRead) val delta = inStream.read(dst, nRead, size - nRead)
if(delta < 0) throw ApngParseError("readBytes: unexpected EoS") if (delta < 0) throw ApngParseError("readBytes: unexpected EoS")
nRead += delta nRead += delta
} }
return dst return dst
} }
private fun readByte() : Int { private fun readByte(): Int {
val b = inStream.read() val b = inStream.read()
if(b == - 1) throw ApngParseError("readByte: unexpected EoS") if (b == -1) throw ApngParseError("readByte: unexpected EoS")
return b and 0xff return b and 0xff
} }
fun readInt32() : Int { fun readInt32(): Int {
val b0 = readByte() val b0 = readByte()
val b1 = readByte() val b1 = readByte()
val b2 = readByte() val b2 = readByte()
val b3 = readByte() val b3 = readByte()
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3 return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
} }
fun readInt32(crc32 : CRC32) : Int { fun readInt32(crc32: CRC32): Int {
val ba = readBytes(4) val ba = readBytes(4)
crc32.update(ba) crc32.update(ba)
val b0 = ba[0].toInt() and 255 val b0 = ba[0].toInt() and 255
val b1 = ba[1].toInt() and 255 val b1 = ba[1].toInt() and 255
val b2 = ba[2].toInt() and 255 val b2 = ba[2].toInt() and 255
val b3 = ba[3].toInt() and 255 val b3 = ba[3].toInt() and 255
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3 return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
} }
fun readUInt32() : Long { fun readUInt32(): Long {
val b0 = readByte() val b0 = readByte()
val b1 = readByte() val b1 = readByte()
val b2 = readByte() val b2 = readByte()
val b3 = readByte() val b3 = readByte()
return (b0.toLong() shl 24) or ((b1 shl 16) or (b2 shl 8) or b3).toLong() return (b0.toLong() shl 24) or ((b1 shl 16) or (b2 shl 8) or b3).toLong()
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -135,6 +135,7 @@ dependencies {
implementation project(':apng_android') implementation project(':apng_android')
implementation fileTree(include: ['*.aar'], dir: 'src/main/libs') implementation fileTree(include: ['*.aar'], dir: 'src/main/libs')
// App1 使 // App1 使
api "org.conscrypt:conscrypt-android:2.5.2" api "org.conscrypt:conscrypt-android:2.5.2"
@ -143,19 +144,48 @@ dependencies {
kapt "com.github.bumptech.glide:compiler:$glideVersion" kapt "com.github.bumptech.glide:compiler:$glideVersion"
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detekt_version") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detekt_version")
testImplementation(project(":base"))
androidTestImplementation(project(":base"))
testApi "androidx.arch.core:core-testing:$arch_version"
testApi "junit:junit:$junit_version"
testApi "org.jetbrains.kotlin:kotlin-test"
testApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
androidTestApi "androidx.test.espresso:espresso-core:3.5.1"
androidTestApi "androidx.test.ext:junit-ktx:1.1.5"
androidTestApi "androidx.test.ext:junit:1.1.5"
androidTestApi "androidx.test.ext:truth:1.5.0"
androidTestApi "androidx.test:core-ktx:1.5.0"
androidTestApi "androidx.test:core:$androidx_test_version"
androidTestApi "androidx.test:runner:1.5.2"
androidTestApi "org.jetbrains.kotlin:kotlin-test"
androidTestApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
// To use android test orchestrator
androidTestUtil "androidx.test:orchestrator:1.4.2"
testApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") {
exclude group: "com.squareup.okio", module: "okio"
exclude group: "com.squareup.okhttp3", module: "okhttp"
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-common"
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jdk8"
}
androidTestApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") {
exclude group: "com.squareup.okio", module: "okio"
exclude group: "com.squareup.okhttp3", module: "okhttp"
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-common"
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jdk8"
}
} }
repositories { repositories {
mavenCentral() mavenCentral()
} }
// detekt
def projectSource = file(projectDir)
def configFile = files("$rootDir/config/detekt/config.yml")
def baselineFile = file("$rootDir/config/detekt/baseline.xml")
def kotlinFiles = "**/*.kt"
def resourceFiles = "**/resources/**"
def buildFiles = "**/build/**"
tasks.register("detektAll", Detekt) { tasks.register("detektAll", Detekt) {
description = "Custom DETEKT build for all modules" description = "Custom DETEKT build for all modules"
@ -173,12 +203,30 @@ tasks.register("detektAll", Detekt) {
// preconfigure defaults // preconfigure defaults
buildUponDefaultConfig = true buildUponDefaultConfig = true
setSource(projectSource) def configFile = files("$rootDir/config/detekt/config.yml")
config.setFrom(configFile) config.setFrom(configFile)
def baselineFile = file("$rootDir/config/detekt/baseline.xml")
if (baselineFile.isFile()) { if (baselineFile.isFile()) {
baseline.set(baselineFile) baseline.set(baselineFile)
} }
include(kotlinFiles)
source = files(
"$rootDir/apng/src",
"$rootDir/apng_android/src",
"$rootDir/app/src",
"$rootDir/base/src",
"$rootDir/colorpicker/src",
"$rootDir/emoji/src",
"$rootDir/icon_material_symbols/src",
"$rootDir/sample_apng/src",
)
// def kotlinFiles = "**/*.kt"
// include(kotlinFiles)
def resourceFiles = "**/resources/**"
def buildFiles = "**/build/**"
exclude(resourceFiles, buildFiles) exclude(resourceFiles, buildFiles)
reports { reports {
html.enabled = true html.enabled = true

View File

@ -1,7 +1,7 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith

View File

@ -1,7 +1,7 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import androidx.test.runner.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import jp.juggler.util.jsonArray import jp.juggler.util.data.*
import org.jetbrains.anko.collections.forEachReversedByIndex import org.jetbrains.anko.collections.forEachReversedByIndex
import org.jetbrains.anko.collections.forEachReversedWithIndex import org.jetbrains.anko.collections.forEachReversedWithIndex
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@ -19,7 +19,7 @@ class JsonArrayForEach {
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun test() { fun test() {
val array = jsonArray { val array = buildJsonArray {
add("a") add("a")
add("b") add("b")
add(null) add(null)
@ -59,4 +59,4 @@ class JsonArrayForEach {
assertEquals(count, 24) assertEquals(count, 24)
} }
} }

View File

@ -1,7 +1,7 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import android.graphics.Color import android.graphics.Color
import androidx.test.runner.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.jrummyapps.android.colorpicker.parseColorString import com.jrummyapps.android.colorpicker.parseColorString
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test

View File

@ -1,6 +1,6 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import androidx.test.runner.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import jp.juggler.subwaytooter.api.entity.TootAccount import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.util.data.asciiPatternString import jp.juggler.util.data.asciiPatternString
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals

View File

@ -2,7 +2,7 @@ package jp.juggler.subwaytooter
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.util.asciiRegex import jp.juggler.util.data.asciiRegex
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test

View File

@ -1,55 +1,48 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import jp.juggler.subwaytooter.api.TootApiCallback import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.entity.Host import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootInstance import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.testutil.MainDispatcherRule
import jp.juggler.subwaytooter.testutil.MockInterceptor
import jp.juggler.subwaytooter.util.SimpleHttpClientImpl import jp.juggler.subwaytooter.util.SimpleHttpClientImpl
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.network.MySslSocketFactory import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.runBlocking import okhttp3.*
import kotlinx.coroutines.withContext
import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class TestTootInstance { class TestTootInstance {
companion object { companion object {
private val log = LogCategory("TestTootInstance") private val log = LogCategory("TestTootInstance")
}
// val cookieJar = JavaNetCookieJar(CookieManager().apply { // テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
// setCookiePolicy(CookiePolicy.ACCEPT_ALL) // プロパティは記述順に初期化されることに注意
// CookieHandler.setDefault(this) @get:Rule
// }) val mainDispatcherRule = MainDispatcherRule()
private val okHttp = OkHttpClient.Builder() private val client by lazy {
.connectTimeout(60.toLong(), TimeUnit.SECONDS) val mockInterceptor = MockInterceptor(
.readTimeout(60.toLong(), TimeUnit.SECONDS) // テストアプリのコンテキスト
.writeTimeout(60.toLong(), TimeUnit.SECONDS) context = InstrumentationRegistry.getInstrumentation().context!!,
.pingInterval(10, TimeUnit.SECONDS) // テストアプリ中のリソースID
.connectionSpecs( rawId = jp.juggler.subwaytooter.test.R.raw.test_toot_instance_mock,
Collections.singletonList( )
ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.allEnabledCipherSuites()
.allEnabledTlsVersions()
.build()
)
)
.sslSocketFactory(MySslSocketFactory, MySslSocketFactory.trustManager)
.build()
private val dummyClientCallback = object : TootApiCallback { val okHttp = OkHttpClient.Builder().addInterceptor(mockInterceptor).build()
val dummyClientCallback = object : TootApiCallback {
override suspend fun isApiCancelled() = false override suspend fun isApiCancelled() = false
override suspend fun publishApiProgress(s: String) { override suspend fun publishApiProgress(s: String) {
@ -61,9 +54,8 @@ class TestTootInstance {
} }
} }
private val appContext = InstrumentationRegistry.getInstrumentation().targetContext!! val appContext = InstrumentationRegistry.getInstrumentation().targetContext!!
TootApiClient(
val client = TootApiClient(
context = appContext, context = appContext,
httpClient = SimpleHttpClientImpl(appContext, okHttp), httpClient = SimpleHttpClientImpl(appContext, okHttp),
callback = dummyClientCallback callback = dummyClientCallback
@ -76,34 +68,26 @@ class TestTootInstance {
*/ */
@Test @Test
fun testWithoutAccount() { fun instanceByHostname() = runTest {
runBlocking { suspend fun a(host: Host) {
withContext(AppDispatchers.io) { val (ti, ri) = TootInstance.getEx(client, hostArg = host)
suspend fun a(host: Host) { assertNull("no error", ri?.error)
val (ti, ri) = TootInstance.getEx(client, hostArg = host) assertNotNull("instance information", ti)
assertNotNull(ti) ti!!.run { log.d("$instanceType $uri $version") }
assertNull(ri?.error)
ti!!.run { log.d("$instanceType $uri $version") }
}
a(Host.parse("mastodon.juggler.jp"))
a(Host.parse("misskey.io"))
}
} }
a(Host.parse("mastodon.juggler.jp"))
a(Host.parse("misskey.io"))
} }
@Test @Test
fun testWithAccount() { fun testWithAccount() = runTest {
runBlocking { suspend fun a(account: SavedAccount) {
withContext(AppDispatchers.io) { val (ti, ri) = TootInstance.getEx(client, account = account)
suspend fun a(account: SavedAccount) { assertNull(ri?.error)
val (ti, ri) = TootInstance.getEx(client, account = account) assertNotNull(ti)
assertNull(ri?.error) ti!!.run { log.d("${account.acct} $instanceType $uri $version") }
assertNotNull(ti)
ti!!.run { log.d("${account.acct} $instanceType $uri $version") }
}
a(SavedAccount(45, "tateisu@mastodon.juggler.jp"))
a(SavedAccount(45, "tateisu@misskey.io", misskeyVersion = 12))
}
} }
a(SavedAccount(45, "tateisu@mastodon.juggler.jp"))
a(SavedAccount(45, "tateisu@misskey.io", misskeyVersion = 12))
} }
} }

View File

@ -1,6 +1,6 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import androidx.test.runner.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import jp.juggler.util.data.CharacterGroup import jp.juggler.util.data.CharacterGroup
import jp.juggler.util.data.WordTrieTree import jp.juggler.util.data.WordTrieTree
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals

View File

@ -1,7 +1,7 @@
package jp.juggler.subwaytooter.api package jp.juggler.subwaytooter.api
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.data.* import jp.juggler.util.data.*

View File

@ -2,18 +2,19 @@
package jp.juggler.subwaytooter.api package jp.juggler.subwaytooter.api
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import jp.juggler.subwaytooter.api.entity.Host import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootInstance import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.testutil.MainDispatcherRule
import jp.juggler.subwaytooter.util.SimpleHttpClient import jp.juggler.subwaytooter.util.SimpleHttpClient
import jp.juggler.util.data.JsonObject import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.buildJsonArray import jp.juggler.util.data.buildJsonArray
import jp.juggler.util.data.buildJsonObject import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.network.MEDIA_TYPE_JSON import jp.juggler.util.network.MEDIA_TYPE_JSON
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
@ -21,6 +22,7 @@ import okio.Buffer
import okio.BufferedSource import okio.BufferedSource
import okio.ByteString import okio.ByteString
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
@ -28,6 +30,12 @@ import java.util.concurrent.atomic.AtomicReference
@Suppress("MemberVisibilityCanPrivate") @Suppress("MemberVisibilityCanPrivate")
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class TestTootApiClient { class TestTootApiClient {
// テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
// プロパティは記述順に初期化されることに注意
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
companion object { companion object {
private val log = LogCategory("TestTootApiClient") private val log = LogCategory("TestTootApiClient")
} }
@ -229,14 +237,21 @@ class TestTootApiClient {
.build() .build()
} }
else -> "/api/meta" -> Response.Builder()
Response.Builder() .request(request)
.request(request) .protocol(Protocol.HTTP_1_1)
.protocol(Protocol.HTTP_1_1) .code(404)
.code(200) .message("not found")
.message("status-message") .body("""{"error":"404 not found"}""".toResponseBody(MEDIA_TYPE_JSON))
.body(request.url.toString().toResponseBody(mediaTypeTextPlain)) .build()
.build()
else -> Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(200)
.message("status-message")
.body(request.url.toString().toResponseBody(mediaTypeTextPlain))
.build()
} }
}, },
@ -519,7 +534,7 @@ class TestTootApiClient {
@Test @Test
fun testIsApiCancelled() { fun testIsApiCancelled() {
runBlocking { runTest {
var flag = 0 var flag = 0
var progressString: String? = null var progressString: String? = null
var progressValue: Int? = null var progressValue: Int? = null
@ -559,7 +574,7 @@ class TestTootApiClient {
@Test @Test
fun testSendRequest() { fun testSendRequest() {
runBlocking { runTest {
val callback = ProgressRecordTootApiCallback() val callback = ProgressRecordTootApiCallback()
@ -637,7 +652,7 @@ class TestTootApiClient {
@Test @Test
fun testReadBodyString() { fun testReadBodyString() {
runBlocking { runTest {
val callback = ProgressRecordTootApiCallback() val callback = ProgressRecordTootApiCallback()
val client = TootApiClient( val client = TootApiClient(
appContext, appContext,
@ -745,7 +760,7 @@ class TestTootApiClient {
@Test @Test
fun testParseString() { fun testParseString() {
runBlocking { runTest {
val callback = ProgressRecordTootApiCallback() val callback = ProgressRecordTootApiCallback()
val client = TootApiClient( val client = TootApiClient(
@ -865,7 +880,7 @@ class TestTootApiClient {
@Test @Test
fun testParseJson() { fun testParseJson() {
runBlocking { runTest {
val callback = ProgressRecordTootApiCallback() val callback = ProgressRecordTootApiCallback()
val client = TootApiClient( val client = TootApiClient(
appContext, appContext,
@ -1046,7 +1061,7 @@ class TestTootApiClient {
@Test @Test
fun testRegisterClient() { fun testRegisterClient() {
runBlocking { runTest {
val callback = ProgressRecordTootApiCallback() val callback = ProgressRecordTootApiCallback()
val client = TootApiClient( val client = TootApiClient(
appContext, appContext,
@ -1064,7 +1079,7 @@ class TestTootApiClient {
assertEquals(null, result?.error) assertEquals(null, result?.error)
var jsonObject = result?.jsonObject var jsonObject = result?.jsonObject
assertNotNull(jsonObject) assertNotNull(jsonObject)
if (jsonObject == null) return@runBlocking if (jsonObject == null) return@runTest
val clientInfo = jsonObject val clientInfo = jsonObject
// clientCredential の作成 // clientCredential の作成
@ -1073,7 +1088,7 @@ class TestTootApiClient {
assertEquals(null, result?.error) assertEquals(null, result?.error)
val clientCredential = result?.string val clientCredential = result?.string
assertNotNull(clientCredential) assertNotNull(clientCredential)
if (clientCredential == null) return@runBlocking if (clientCredential == null) return@runTest
clientInfo[TootApiClient.KEY_CLIENT_CREDENTIAL] = clientCredential clientInfo[TootApiClient.KEY_CLIENT_CREDENTIAL] = clientCredential
// clientCredential の検証 // clientCredential の検証
@ -1082,7 +1097,7 @@ class TestTootApiClient {
assertEquals(null, result?.error) assertEquals(null, result?.error)
jsonObject = result?.jsonObject jsonObject = result?.jsonObject
assertNotNull(jsonObject) // 中味は別に見てない。jsonObjectなら良いらしい assertNotNull(jsonObject) // 中味は別に見てない。jsonObjectなら良いらしい
if (jsonObject == null) return@runBlocking if (jsonObject == null) return@runTest
var url: String? var url: String?
@ -1095,7 +1110,7 @@ class TestTootApiClient {
result = client.authentication1(clientName) result = client.authentication1(clientName)
url = result?.string url = result?.string
assertNotNull(url) assertNotNull(url)
if (url == null) return@runBlocking if (url == null) return@runTest
println(url) println(url)
// ブラウザからコールバックで受け取ったcodeを処理する // ブラウザからコールバックで受け取ったcodeを処理する
@ -1103,29 +1118,29 @@ class TestTootApiClient {
result = client.authentication2Mastodon(clientName, "DUMMY_CODE", refToken) result = client.authentication2Mastodon(clientName, "DUMMY_CODE", refToken)
jsonObject = result?.jsonObject jsonObject = result?.jsonObject
assertNotNull(jsonObject) assertNotNull(jsonObject)
if (jsonObject == null) return@runBlocking if (jsonObject == null) return@runTest
println(jsonObject.toString()) println(jsonObject.toString())
// 認証できたならアクセストークンがある // 認証できたならアクセストークンがある
val tokenInfo = result?.tokenInfo val tokenInfo = result?.tokenInfo
assertNotNull(tokenInfo) assertNotNull(tokenInfo)
if (tokenInfo == null) return@runBlocking if (tokenInfo == null) return@runTest
val accessToken = tokenInfo.string("access_token") val accessToken = tokenInfo.string("access_token")
assertNotNull(accessToken) assertNotNull(accessToken)
if (accessToken == null) return@runBlocking if (accessToken == null) return@runTest
// アカウント手動入力でログインする場合はこの関数を直接呼び出す // アカウント手動入力でログインする場合はこの関数を直接呼び出す
result = client.getUserCredential(accessToken, tokenInfo) result = client.getUserCredential(accessToken, tokenInfo)
jsonObject = result?.jsonObject jsonObject = result?.jsonObject
assertNotNull(jsonObject) assertNotNull(jsonObject)
if (jsonObject == null) return@runBlocking if (jsonObject == null) return@runTest
println(jsonObject.toString()) println(jsonObject.toString())
} }
} }
@Test @Test
fun testGetInstanceInformation() { fun testGetInstanceInformation() {
runBlocking { runTest {
val callback = ProgressRecordTootApiCallback() val callback = ProgressRecordTootApiCallback()
val client = TootApiClient( val client = TootApiClient(
appContext, appContext,
@ -1135,8 +1150,8 @@ class TestTootApiClient {
val instance = Host.parse("unit-test") val instance = Host.parse("unit-test")
client.apiHost = instance client.apiHost = instance
val (instanceInfo, instanceResult) = TootInstance.get(client) val (instanceInfo, instanceResult) = TootInstance.get(client)
assertNotNull(instanceInfo) assertNull("no error", instanceResult?.error)
assertNotNull(instanceResult) assertNotNull("instance info", instanceInfo)
val json = instanceResult?.jsonObject val json = instanceResult?.jsonObject
if (json != null) println(json.toString()) if (json != null) println(json.toString())
} }
@ -1144,7 +1159,7 @@ class TestTootApiClient {
@Test @Test
fun testGetHttp() { fun testGetHttp() {
runBlocking { runTest {
val callback = ProgressRecordTootApiCallback() val callback = ProgressRecordTootApiCallback()
val client = TootApiClient( val client = TootApiClient(
appContext, appContext,
@ -1160,7 +1175,7 @@ class TestTootApiClient {
@Test @Test
fun testRequest() { fun testRequest() {
runBlocking { runTest {
val tokenInfo = JsonObject() val tokenInfo = JsonObject()
tokenInfo["access_token"] = "DUMMY_ACCESS_TOKEN" tokenInfo["access_token"] = "DUMMY_ACCESS_TOKEN"
@ -1188,7 +1203,7 @@ class TestTootApiClient {
@Test @Test
fun testWebSocket() { fun testWebSocket() {
runBlocking { runTest {
val tokenInfo = buildJsonObject { val tokenInfo = buildJsonObject {
put("access_token", "DUMMY_ACCESS_TOKEN") put("access_token", "DUMMY_ACCESS_TOKEN")
} }

View File

@ -1,7 +1,7 @@
package jp.juggler.subwaytooter.api.entity package jp.juggler.subwaytooter.api.entity
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.data.* import jp.juggler.util.data.*

View File

@ -1,6 +1,6 @@
package jp.juggler.subwaytooter.api.entity package jp.juggler.subwaytooter.api.entity
import androidx.test.runner.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.LinkHelper
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test

View File

@ -1,6 +1,6 @@
package jp.juggler.subwaytooter.database package jp.juggler.subwaytooter.database
import androidx.test.runner.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith

View File

@ -4,7 +4,7 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper import android.database.sqlite.SQLiteOpenHelper
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import jp.juggler.subwaytooter.global.DB_VERSION import jp.juggler.subwaytooter.global.DB_VERSION
import jp.juggler.subwaytooter.global.TABLE_LIST import jp.juggler.subwaytooter.global.TABLE_LIST
import org.junit.Assert.assertNull import org.junit.Assert.assertNull

View File

@ -0,0 +1,32 @@
package jp.juggler.subwaytooter.testutil
import jp.juggler.util.coroutine.AppDispatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
/**
* Dispatchers.Main のテスト中の置き換えを複数テストで衝突しないようにルール化する
* https://developer.android.com/kotlin/coroutines/test?hl=ja
*/
@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
/**
* UnconfinedTestDispatcher StandardTestDispatcher のどちらかを指定する
*/
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
AppDispatchers.setTest(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}

View File

@ -0,0 +1,57 @@
package jp.juggler.subwaytooter.testutil
import android.content.Context
import androidx.annotation.RawRes
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.decodeJsonObject
import jp.juggler.util.data.decodeUTF8
import jp.juggler.util.data.encodeUTF8
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import java.io.IOException
class MockInterceptor(private val mockJsonMap: JsonObject) : Interceptor {
constructor(context: Context, @RawRes rawId: Int) : this(
context.resources.openRawResource(rawId)
.use { it.readBytes() }
.decodeUTF8()
.decodeJsonObject()
)
override fun intercept(chain: Interceptor.Chain): Response {
val url = chain.request().url.toString()
return when (val resultValue = mockJsonMap[url]) {
null -> throw IOException("missing mockJson for $url")
is JsonObject -> Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_2)
.code(200)
.message("OK")
.body(
resultValue.toString().encodeUTF8()
.toResponseBody("application/json".toMediaType())
).build()
is Number -> Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_2)
.code(resultValue.toInt())
.message("error $resultValue")
.body(
"""{"error":$resultValue}""".encodeUTF8()
.toResponseBody("application/json".toMediaType())
).build()
else -> Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_2)
.code(500)
.message("unknonw result type: $resultValue")
.body(
"""{"error":$resultValue}""".encodeUTF8()
.toResponseBody("application/json".toMediaType())
).build()
}
}
}

View File

@ -1,6 +1,6 @@
package jp.juggler.subwaytooter.util package jp.juggler.subwaytooter.util
import androidx.test.runner.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith

View File

@ -1,9 +1,9 @@
package jp.juggler.subwaytooter.util package jp.juggler.subwaytooter.util
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import jp.juggler.subwaytooter.api.entity.Host import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.util.neatSpaces import jp.juggler.util.data.*
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -17,7 +17,7 @@ class TestHtmlDecoder {
val start: Int, val start: Int,
val end: Int, val end: Int,
val flags: Int, val flags: Int,
val text: String val text: String,
) { ) {
override fun toString() = "[$start..$end) $flags ${span.javaClass.simpleName} $text" override fun toString() = "[$start..$end) $flags ${span.javaClass.simpleName} $text"
} }

View File

@ -0,0 +1,226 @@
{
"https://mastodon.juggler.jp/api/v1/instance": {
"uri": "mastodon.juggler.jp",
"title": "juggler.jp Mastodon サービス",
"short_description": "日本語で楽しめるMastodonサーバを提供しています。",
"description": "日本語で楽しめるMastodonサーバを提供しています。\u003ca href=\"https://mastodon.juggler.jp/terms\"\u003e利用規約\u003c/a\u003eを読んでからサインアップしてください。",
"email": "tateisu+juggler@gmail.com",
"version": "4.0.2",
"urls": {
"streaming_api": "wss://mastodon.juggler.jp"
},
"stats": {
"user_count": 1799,
"status_count": 725461,
"domain_count": 11626
},
"thumbnail": "https://m1j.zzz.ac/site_uploads/files/000/000/001/@1x/6f06b912bd963e28.png",
"languages": [
"ja"
],
"registrations": true,
"approval_required": false,
"invites_enabled": true,
"configuration": {
"accounts": {
"max_featured_tags": 10
},
"statuses": {
"max_characters": 500,
"max_media_attachments": 4,
"characters_reserved_per_url": 23
},
"media_attachments": {
"supported_mime_types": [
"image/jpeg",
"image/png",
"image/gif",
"image/heic",
"image/heif",
"image/webp",
"image/avif",
"video/webm",
"video/mp4",
"video/quicktime",
"video/ogg",
"audio/wave",
"audio/wav",
"audio/x-wav",
"audio/x-pn-wave",
"audio/vnd.wave",
"audio/ogg",
"audio/vorbis",
"audio/mpeg",
"audio/mp3",
"audio/webm",
"audio/flac",
"audio/aac",
"audio/m4a",
"audio/x-m4a",
"audio/mp4",
"audio/3gpp",
"video/x-ms-asf"
],
"image_size_limit": 10485760,
"image_matrix_limit": 16777216,
"video_size_limit": 41943040,
"video_frame_rate_limit": 60,
"video_matrix_limit": 2304000
},
"polls": {
"max_options": 4,
"max_characters_per_option": 50,
"min_expiration": 300,
"max_expiration": 2629746
}
},
"contact_account": {
"id": "33698",
"username": "juggler",
"acct": "juggler",
"display_name": "mastodon.juggler.jp運営情報",
"locked": false,
"bot": false,
"discoverable": true,
"group": false,
"created_at": "2017-09-10T00:00:00.000Z",
"note": "\u003cp\u003emastodon.juggler.jp の運営情報アカウントです。\u003cbr /\u003eメンテナンス中の進捗は \u003ca href=\"https://fedibird.com/@tateisu\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003efedibird.com/@tateisu\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e や \u003ca href=\"https://twitter.com/tateisu\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003etwitter.com/tateisu\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e で報告するかもしれません。\u003cbr /\u003e\u003ca href=\"https://mastodon.juggler.jp/tags/%E9%81%8B%E5%96%B6%E6%83%85%E5%A0%B1\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003e運営情報\u003c/span\u003e\u003c/a\u003e \u003ca href=\"https://mastodon.juggler.jp/tags/Mastodon\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003eMastodon\u003c/span\u003e\u003c/a\u003e\u003c/p\u003e",
"url": "https://mastodon.juggler.jp/@juggler",
"avatar": "https://m1j.zzz.ac/accounts/avatars/000/033/698/original/a2db8e1efe4dec82.jpg",
"avatar_static": "https://m1j.zzz.ac/accounts/avatars/000/033/698/original/a2db8e1efe4dec82.jpg",
"header": "https://m1j.zzz.ac/accounts/headers/000/033/698/original/6396b4dcb2f3b580.png",
"header_static": "https://m1j.zzz.ac/accounts/headers/000/033/698/original/6396b4dcb2f3b580.png",
"followers_count": 605,
"following_count": 0,
"statuses_count": 215,
"last_status_at": "2023-01-07",
"noindex": false,
"emojis": [],
"fields": [
{
"name": "Fantia",
"value": "\u003ca href=\"https://fantia.jp/fanclubs/8239\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003efantia.jp/fanclubs/8239\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e",
"verified_at": null
},
{
"name": "Amazon干し芋",
"value": "\u003ca href=\"http://amzn.asia/axmNivM\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttp://\u003c/span\u003e\u003cspan class=\"\"\u003eamzn.asia/axmNivM\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e",
"verified_at": null
}
]
},
"rules": [
{
"id": "1",
"text": "日本の法律や判例で禁止されてることはダメです。ただし親告罪は関係者からの通報がない場合は黙認する場合があります。"
},
{
"id": "3",
"text": "(ネット上のアバターを含む)実在する人格へのなりすましを禁止します。"
},
{
"id": "4",
"text": "サーバ運営を妨害する行為はダメです。これはアカウント大量作成、無差別フォロー、無差別ブースト、無差別ファボを含みます。\r\n"
},
{
"id": "7",
"text": "(GDPR免責) あなたがデータを削除したい場合、我々は可能な範囲で対応します。しかし連合先のサーバ上のデータの削除について完全な責任を持つことは非常に困難です。これはEUのGDPRに厳密には適合しません。ユーザはこれに同意した上でアカウントを作成したものとみなします。"
},
{
"id": "8",
"text": "公開TLからのサイレンス対象。セールス、政治活動、宗教勧誘、SEO目的の投稿が多い人。公開TLに自動投稿するボット。オカルトやスピリチュアルや愚痴ばかり書いて住民からの呼びかけにはほとんど答えない人。アバターアイコンや名前が食事時に見たくない感じの人。その他管理者が公開TLに不適切であると判断したアカウントはサイレンス、凍結、停止などの処置を行う場合があります。"
},
{
"id": "10",
"text": "年齢が14歳未満のユーザはこのサーバを利用できません。"
}
]
},
"https://misskey.io/api/v1/instance": 404,
"https://misskey.io/api/meta": {
"driveCapacityPerLocalUserMb": 0,
"driveCapacityPerRemoteUserMb": 0,
"cacheRemoteFiles": true,
"emailRequiredForSignup": true,
"enableHcaptcha": true,
"hcaptchaSiteKey": "string",
"enableRecaptcha": true,
"recaptchaSiteKey": "string",
"swPublickey": "string",
"mascotImageUrl": "/assets/ai.png",
"bannerUrl": "string",
"errorImageUrl": "https://xn--931a.moe/aiart/yubitun.png",
"iconUrl": "string",
"maxNoteTextLength": 0,
"emojis": [
{
"id": "string",
"aliases": [
"string"
],
"category": "string",
"host": "string",
"url": "string"
}
],
"ads": [
{
"place": "string",
"url": "string",
"imageUrl": "string"
}
],
"enableEmail": true,
"enableTwitterIntegration": true,
"enableGithubIntegration": true,
"enableDiscordIntegration": true,
"enableServiceWorker": true,
"translatorAvailable": true,
"proxyAccountName": "string",
"userStarForReactionFallback": true,
"pinnedUsers": [
"string"
],
"hiddenTags": [
"string"
],
"blockedHosts": [
"string"
],
"hcaptchaSecretKey": "string",
"recaptchaSecretKey": "string",
"sensitiveMediaDetection": "string",
"sensitiveMediaDetectionSensitivity": "string",
"setSensitiveFlagAutomatically": true,
"enableSensitiveMediaDetectionForVideos": true,
"proxyAccountId": "string",
"twitterConsumerKey": "string",
"twitterConsumerSecret": "string",
"githubClientId": "string",
"githubClientSecret": "string",
"discordClientId": "string",
"discordClientSecret": "string",
"summaryProxy": "string",
"email": "string",
"smtpSecure": true,
"smtpHost": "string",
"smtpPort": "string",
"smtpUser": "string",
"smtpPass": "string",
"swPrivateKey": "string",
"useObjectStorage": true,
"objectStorageBaseUrl": "string",
"objectStorageBucket": "string",
"objectStoragePrefix": "string",
"objectStorageEndpoint": "string",
"objectStorageRegion": "string",
"objectStoragePort": 0,
"objectStorageAccessKey": "string",
"objectStorageSecretKey": "string",
"objectStorageUseSSL": true,
"objectStorageUseProxy": true,
"objectStorageSetPublicRead": true,
"enableIpLogging": true,
"enableActiveEmailValidation": true
}
}

View File

@ -23,7 +23,6 @@ import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.network.toFormRequestBody import jp.juggler.util.network.toFormRequestBody
import jp.juggler.util.network.toPost import jp.juggler.util.network.toPost
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
private val log = LogCategory("Action_Tag") private val log = LogCategory("Action_Tag")

View File

@ -43,7 +43,6 @@ import jp.juggler.util.log.showToast
import jp.juggler.util.ui.activity import jp.juggler.util.ui.activity
import jp.juggler.util.ui.attrColor import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.createColoredDrawable import jp.juggler.util.ui.createColoredDrawable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.anko.backgroundColor import org.jetbrains.anko.backgroundColor
import java.lang.ref.WeakReference import java.lang.ref.WeakReference

View File

@ -8,6 +8,7 @@ import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.subwaytooter.util.VersionString import jp.juggler.subwaytooter.util.VersionString
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
import jp.juggler.util.coroutine.launchDefault import jp.juggler.util.coroutine.launchDefault
import jp.juggler.util.data.* import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -15,7 +16,6 @@ import jp.juggler.util.log.withCaption
import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.network.toPostRequestBuilder
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
import okhttp3.Request import okhttp3.Request
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
@ -431,7 +431,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
try { try {
val req = requestQueue.receive() val req = requestQueue.receive()
val r = try { val r = try {
withTimeout(30000L) { withTimeoutSafe(30000L) {
handleRequest(req) handleRequest(req)
} }
} catch (ex: Throwable) { } catch (ex: Throwable) {
@ -481,7 +481,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
val cacheEntry = (hostArg ?: account?.apiHost ?: client.apiHost)?.getCacheEntry() val cacheEntry = (hostArg ?: account?.apiHost ?: client.apiHost)?.getCacheEntry()
?: return tiError("missing host.") ?: return tiError("missing host.")
return withTimeout(30000L) { return withTimeoutSafe(30000L) {
suspendCoroutine { cont -> suspendCoroutine { cont ->
QueuedRequest(cont, allowPixelfed) { cached -> QueuedRequest(cont, allowPixelfed) { cached ->

View File

@ -8,11 +8,11 @@ import android.net.wifi.WifiManager
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.App1
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
import jp.juggler.util.log.* import jp.juggler.util.log.*
import jp.juggler.util.systemService import jp.juggler.util.systemService
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
class CheckerWakeLocks(contextArg: Context) { class CheckerWakeLocks(contextArg: Context) {
companion object { companion object {
@ -118,7 +118,7 @@ class CheckerWakeLocks(contextArg: Context) {
suspend fun checkConnection() { suspend fun checkConnection() {
var connectionState: String? = null var connectionState: String? = null
try { try {
withTimeout(10000L) { withTimeoutSafe(10000L) {
while (true) { while (true) {
connectionState = appState.networkTracker.connectionState connectionState = appState.networkTracker.connectionState
?: break // null if connected ?: break // null if connected

View File

@ -29,7 +29,6 @@ import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.network.toPut import jp.juggler.util.network.toPut
import jp.juggler.util.network.toPutRequestBuilder import jp.juggler.util.network.toPutRequestBuilder
import jp.juggler.util.ui.* import jp.juggler.util.ui.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View File

@ -21,7 +21,6 @@ import jp.juggler.util.network.MEDIA_TYPE_JSON
import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.ui.* import jp.juggler.util.ui.*
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Request import okhttp3.Request

View File

@ -1,10 +1,10 @@
package jp.juggler.subwaytooter.util package jp.juggler.subwaytooter.util
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
import jp.juggler.util.coroutine.launchDefault import jp.juggler.util.coroutine.launchDefault
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.withTimeout
abstract class WorkerBase( abstract class WorkerBase(
private val waiter: Channel<Unit> = Channel(capacity = Channel.CONFLATED), private val waiter: Channel<Unit> = Channel(capacity = Channel.CONFLATED),
@ -17,7 +17,7 @@ abstract class WorkerBase(
abstract suspend fun run() abstract suspend fun run()
suspend fun waitEx(ms: Long) = try { suspend fun waitEx(ms: Long) = try {
withTimeout(ms) { waiter.receive() } withTimeoutSafe(ms) { waiter.receive() }
} catch (ignored: TimeoutCancellationException) { } catch (ignored: TimeoutCancellationException) {
null null
} }

View File

@ -111,11 +111,18 @@ dependencies {
testApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version" testApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
androidTestApi "androidx.test.espresso:espresso-core:3.5.1" androidTestApi "androidx.test.espresso:espresso-core:3.5.1"
androidTestApi "androidx.test.ext:junit-ktx:1.1.5"
androidTestApi "androidx.test.ext:junit:1.1.5" androidTestApi "androidx.test.ext:junit:1.1.5"
androidTestApi "androidx.test.ext:truth:1.5.0"
androidTestApi "androidx.test:core-ktx:1.5.0"
androidTestApi "androidx.test:core:$androidx_test_version" androidTestApi "androidx.test:core:$androidx_test_version"
androidTestApi "androidx.test:runner:1.5.2"
androidTestApi "org.jetbrains.kotlin:kotlin-test" androidTestApi "org.jetbrains.kotlin:kotlin-test"
androidTestApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version" androidTestApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
// To use android test orchestrator
androidTestUtil "androidx.test:orchestrator:1.4.2"
testApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") { testApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") {
exclude group: "com.squareup.okio", module: "okio" exclude group: "com.squareup.okio", module: "okio"
exclude group: "com.squareup.okhttp3", module: "okhttp" exclude group: "com.squareup.okhttp3", module: "okhttp"

View File

@ -1,16 +1,14 @@
package jp.juggler.base package jp.juggler.base
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import jp.juggler.base.JugglerBase.Companion.jugglerBase import jp.juggler.base.JugglerBase.Companion.jugglerBase
import jp.juggler.base.JugglerBase.Companion.jugglerBaseNullable import jp.juggler.base.JugglerBase.Companion.jugglerBaseNullable
import jp.juggler.base.JugglerBase.Companion.prepareJugglerBase import jp.juggler.base.JugglerBase.Companion.prepareJugglerBase
import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.Assert.*
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
* *
@ -23,6 +21,6 @@ class JugglerBaseTest {
assertNotNull("JubblerBase is initialized for a test.", jugglerBaseNullable) assertNotNull("JubblerBase is initialized for a test.", jugglerBaseNullable)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
appContext.prepareJugglerBase appContext.prepareJugglerBase
assertNotNull( "JubblerBase is initialized after prepare.",jugglerBase) assertNotNull("JubblerBase is initialized after prepare.", jugglerBase)
} }
} }

View File

@ -1,7 +1,6 @@
package jp.juggler.util.coroutine package jp.juggler.util.coroutine
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
/** /**
* Test時にdispatcherを差し替えられるようにする * Test時にdispatcherを差し替えられるようにする
@ -33,4 +32,15 @@ object AppDispatchers {
default = testDispatcher default = testDispatcher
io = testDispatcher io = testDispatcher
} }
/**
* withTimeout はrunTest内部だと即座に例外を出すので
* テスト中はタイムアウトをチェックしないようにする
* https://stackoverflow.com/questions/70658926/how-to-use-kotlinx-coroutines-withtimeout-in-kotlinx-coroutines-test-runtest
*/
suspend fun <T> withTimeoutSafe(timeMillis: Long, block: suspend CoroutineScope.() -> T) =
when (io) {
Dispatchers.IO -> withTimeout(timeMillis, block)
else -> coroutineScope { block() }
}
} }

View File

@ -3,7 +3,6 @@ package jp.juggler.util.coroutine
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext

View File

@ -1,7 +1,5 @@
package jp.juggler.util.data package jp.juggler.util.data
import java.util.LinkedHashMap
// same as x?.let{ dst.add(it) } // same as x?.let{ dst.add(it) }
fun <T> T.addTo(dst: ArrayList<T>) = dst.add(this) fun <T> T.addTo(dst: ArrayList<T>) = dst.add(this)
@ -34,4 +32,3 @@ fun <T : Any> MutableCollection<T>.removeFirst(check: (T) -> Boolean): T? {
} }
return null return null
} }

View File

@ -59,8 +59,8 @@ fun Cursor.getBlobOrNull(keyIdx: Int) =
fun Cursor.getBlobOrNull(key: String) = fun Cursor.getBlobOrNull(key: String) =
getBlobOrNull(getColumnIndex(key)) getBlobOrNull(getColumnIndex(key))
fun Cursor.columnIndexOrThrow(key:String)= fun Cursor.columnIndexOrThrow(key: String) =
getColumnIndex(key).takeIf{it>=0L} ?: error("missing column $key") getColumnIndex(key).takeIf { it >= 0L } ?: error("missing column $key")
///////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////

View File

@ -7,8 +7,6 @@ import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.annotation.RawRes import androidx.annotation.RawRes
import jp.juggler.util.data.getStringOrNull
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import java.io.InputStream import java.io.InputStream

View File

@ -69,7 +69,7 @@ class WordTrieTree {
// 単語の追加 // 単語の追加
fun add( fun add(
s: String, s: String,
tag:Any?=null, tag: Any? = null,
validator: (src: CharSequence, start: Int, end: Int) -> Boolean = EMPTY_VALIDATOR, validator: (src: CharSequence, start: Int, end: Int) -> Boolean = EMPTY_VALIDATOR,
) { ) {
val t = CharacterGroup.Tokenizer().reset(s, 0, s.length) val t = CharacterGroup.Tokenizer().reset(s, 0, s.length)
@ -92,9 +92,9 @@ class WordTrieTree {
} }
// タグを覚える // タグを覚える
if(tag!=null){ if (tag != null) {
val tags = node.matchTags val tags = node.matchTags
?: ArrayList<Any>().also{ node.matchTags = it} ?: ArrayList<Any>().also { node.matchTags = it }
tags.add(tag) tags.add(tag)
} }
@ -136,7 +136,7 @@ class WordTrieTree {
if (matchWord != null && node.validator(t.text, start, t.offset)) { if (matchWord != null && node.validator(t.text, start, t.offset)) {
// マッチしたことを覚えておく // マッチしたことを覚えておく
dst = Match(start, t.offset, matchWord ,node.matchTags) dst = Match(start, t.offset, matchWord, node.matchTags)
// ミュート用途の場合、ひとつでも単語にマッチすればより長い探索は必要ない // ミュート用途の場合、ひとつでも単語にマッチすればより長い探索は必要ない
if (allowShortMatch) break if (allowShortMatch) break
@ -205,7 +205,7 @@ class WordTrieTree {
} }
@OptIn(ExperimentalContracts::class) @OptIn(ExperimentalContracts::class)
fun WordTrieTree?.isNullOrEmpty() :Boolean { fun WordTrieTree?.isNullOrEmpty(): Boolean {
contract { contract {
returns(false) implies (this@isNullOrEmpty != null) returns(false) implies (this@isNullOrEmpty != null)
} }

View File

@ -12,11 +12,11 @@ import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
import jp.juggler.util.coroutine.runOnMainLooper import jp.juggler.util.coroutine.runOnMainLooper
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import me.drakeet.support.toast.ToastCompat import me.drakeet.support.toast.ToastCompat
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -84,7 +84,7 @@ fun initializeToastUtils(app: Application) {
*/ */
suspend fun Animation.startAndAwait(duration: Long, v: View) = suspend fun Animation.startAndAwait(duration: Long, v: View) =
try { try {
withTimeout(duration + 333L) { withTimeoutSafe(duration + 333L) {
suspendCancellableCoroutine { cont -> suspendCancellableCoroutine { cont ->
v.clearAnimation() v.clearAnimation()
this@startAndAwait.duration = duration this@startAndAwait.duration = duration

View File

@ -37,7 +37,6 @@ internal class AlphaPatternDrawable(private val rectangleSize: Int) : Drawable()
*/ */
private var bitmap: Bitmap? = null private var bitmap: Bitmap? = null
/** /**
* This will generate a bitmap with the pattern as big as the rectangle we were allow to draw on. * 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 * 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

View File

@ -28,7 +28,7 @@ internal class ColorPaletteAdapter(
val colors: IntArray, val colors: IntArray,
var selectedPosition: Int, var selectedPosition: Int,
@ColorShape val colorShape: Int, @ColorShape val colorShape: Int,
val listener: (Int)->Unit val listener: (Int) -> Unit,
) : BaseAdapter() { ) : BaseAdapter() {
override fun getCount(): Int = colors.size override fun getCount(): Int = colors.size

View File

@ -37,7 +37,7 @@ import androidx.core.view.ViewCompat
class ColorPanelView @JvmOverloads constructor( class ColorPanelView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = 0 defStyle: Int = 0,
) : View(context, attrs, defStyle) { ) : View(context, attrs, defStyle) {
companion object { companion object {
@ -97,7 +97,6 @@ class ColorPanelView @JvmOverloads constructor(
} }
} }
init { init {
val a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView) val a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView)
shape = a.getInt(R.styleable.ColorPanelView_cpv_colorShape, ColorShape.CIRCLE) shape = a.getInt(R.styleable.ColorPanelView_cpv_colorShape, ColorShape.CIRCLE)
@ -117,11 +116,8 @@ class ColorPanelView @JvmOverloads constructor(
borderColor = typedArray.getColor(0, borderColor) borderColor = typedArray.getColor(0, borderColor)
typedArray.recycle() typedArray.recycle()
} }
} }
public override fun onSaveInstanceState(): Parcelable { public override fun onSaveInstanceState(): Parcelable {
val state = Bundle() val state = Bundle()
state.putParcelable("instanceState", super.onSaveInstanceState()) state.putParcelable("instanceState", super.onSaveInstanceState())
@ -138,7 +134,6 @@ class ColorPanelView @JvmOverloads constructor(
} }
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
borderPaint.color = borderColor borderPaint.color = borderColor
colorPaint.color = color colorPaint.color = color
@ -236,7 +231,6 @@ class ColorPanelView @JvmOverloads constructor(
alphaPattern.setBounds(left, top, right, bottom) alphaPattern.setBounds(left, top, right, bottom)
} }
/** /**
* Set the original color. This is only used for previewing colors. * Set the original color. This is only used for previewing colors.
* *
@ -264,13 +258,12 @@ class ColorPanelView @JvmOverloads constructor(
val screenWidth = context.resources.displayMetrics.widthPixels val screenWidth = context.resources.displayMetrics.widthPixels
referenceX = screenWidth - referenceX // mirror referenceX = screenWidth - referenceX // mirror
} }
val hint = StringBuilder("#") val hexText = when {
if (Color.alpha(color) != 255) { Color.alpha(color) == 255 -> "%06X".format(color and 0xFFFFFF)
hint.append(Integer.toHexString(color).uppercase()) else -> Integer.toHexString(color)
} else {
hint.append(String.format("%06X", 0xFFFFFF and color).uppercase())
} }
val cheatSheet = Toast.makeText(context, hint.toString(), Toast.LENGTH_SHORT) val hint = "#${hexText.uppercase()}"
val cheatSheet = Toast.makeText(context, hint, Toast.LENGTH_SHORT)
if (midy < displayFrame.height()) { if (midy < displayFrame.height()) {
// Show along the top; follow action buttons // Show along the top; follow action buttons
cheatSheet.setGravity( cheatSheet.setGravity(

View File

@ -265,11 +265,10 @@ class ColorPickerDialog :
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
// region Custom Picker // region Custom Picker
private fun createPickerView(): View { private fun createPickerView(): View {
val args = arguments ?: throw RuntimeException("createPickerView: args is null") val args = arguments ?: error("createPickerView: args is null")
val activity = activity ?: throw RuntimeException("createPickerView: activity is null") val activity = activity ?: error("createPickerView: activity is null")
val contentView = View.inflate(getActivity(), R.layout.cpv_dialog_color_picker, null) val contentView = View.inflate(getActivity(), R.layout.cpv_dialog_color_picker, null)
colorPicker = contentView.findViewById(R.id.cpv_color_picker_view) colorPicker = contentView.findViewById(R.id.cpv_color_picker_view)
val oldColorPanel: ColorPanelView = contentView.findViewById(R.id.cpv_color_panel_old) val oldColorPanel: ColorPanelView = contentView.findViewById(R.id.cpv_color_panel_old)
@ -376,14 +375,13 @@ class ColorPickerDialog :
} }
private fun setHex(color: Int) { private fun setHex(color: Int) {
if (showAlphaSlider) { val hexText = when {
hexEditText!!.setText(String.format("%08X", color)) showAlphaSlider -> "%08X".format(color)
} else { else -> "%06X".format(color and 0xFFFFFF)
hexEditText!!.setText(String.format("%06X", 0xFFFFFF and color))
} }
hexEditText?.setText(hexText)
} }
// -- endregion -- // -- endregion --
// region Presets Picker // region Presets Picker
private fun createPresetsView(): View { private fun createPresetsView(): View {
@ -399,13 +397,13 @@ class ColorPickerDialog :
shadesLayout?.visibility = View.GONE shadesLayout?.visibility = View.GONE
contentView.findViewById<View>(R.id.shades_divider).visibility = View.GONE contentView.findViewById<View>(R.id.shades_divider).visibility = View.GONE
} }
adapter = ColorPaletteAdapter(presets, selectedItemPosition, colorShape){ adapter = ColorPaletteAdapter(presets, selectedItemPosition, colorShape) {
when(it){ when (it) {
color -> { color -> {
colorPickerDialogListener?.onColorSelected(dialogId, color) colorPickerDialogListener?.onColorSelected(dialogId, color)
dismiss() dismiss()
} }
else ->{ else -> {
color = it color = it
if (showColorShades) { if (showColorShades) {
createColorShades(color) createColorShades(color)
@ -519,18 +517,18 @@ class ColorPickerDialog :
} }
private fun shadeColor(@ColorInt color: Int, percent: Double): Int { private fun shadeColor(@ColorInt color: Int, percent: Double): Int {
val hex = String.format("#%06X", 0xFFFFFF and color) val hex = "#%06X".format(color and 0xFFFFFF)
val f = hex.substring(1).toLong(16) val f = hex.substring(1).toLong(16)
val t = (if (percent < 0) 0 else 255).toDouble() val t = (if (percent < 0) 0 else 255).toDouble()
val p = if (percent < 0) percent * -1 else percent val p = if (percent < 0) percent * -1 else percent
val R = f shr 16 val cR = f shr 16
val G = f shr 8 and 0x00FF val cG = f shr 8 and 0x00FF
val B = f and 0x0000FF val cB = f and 0x0000FF
return Color.argb( return Color.argb(
Color.alpha(color), Color.alpha(color),
((t - R) * p).roundToInt() + R.toInt(), ((t - cR) * p).roundToInt() + cR.toInt(),
((t - G) * p).roundToInt() + G.toInt(), ((t - cG) * p).roundToInt() + cG.toInt(),
((t - B) * p).roundToInt() + B.toInt(), ((t - cB) * p).roundToInt() + cB.toInt(),
) )
} }

View File

@ -502,13 +502,13 @@ class ColorPickerView @JvmOverloads constructor(
return p return p
} }
private fun satValToPoint(sat: Float, `val`: Float): Point { private fun satValToPoint(sat: Float, inValue: Float): Point {
val rect = satValRect val rect = satValRect!!
val height = rect!!.height().toFloat() val height = rect.height().toFloat()
val width = rect.width().toFloat() val width = rect.width().toFloat()
val p = Point() val p = Point()
p.x = (sat * width + rect.left).toInt() p.x = (sat * width + rect.left).toInt()
p.y = ((1f - `val`) * height + rect.top).toInt() p.y = ((1f - inValue) * height + rect.top).toInt()
return p return p
} }

View File

@ -1,24 +0,0 @@
package jp.juggler.icon_material_symbols
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("jp.juggler.icon_material_symbols.test", appContext.packageName)
}
}

View File

@ -1,17 +0,0 @@
package jp.juggler.icon_material_symbols
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -78,11 +78,11 @@ class ActList : AppCompatActivity(), CoroutineScope {
grantResults: IntArray, grantResults: IntArray,
) { ) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST_CODE_STORAGE) { // if (requestCode == PERMISSION_REQUEST_CODE_STORAGE) {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) { // if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
// 特に何もしてないらしい // // 特に何もしてないらしい
} // }
} // }
} }
private fun load() = launch { private fun load() = launch {
@ -153,14 +153,14 @@ class ActList : AppCompatActivity(), CoroutineScope {
inner class MyViewHolder( inner class MyViewHolder(
viewRoot: View, viewRoot: View,
_activity: ActList, actList: ActList,
) { ) {
private val tvCaption: TextView = viewRoot.findViewById(R.id.tvCaption) private val tvCaption: TextView = viewRoot.findViewById(R.id.tvCaption)
private val apngView: ApngView = viewRoot.findViewById(R.id.apngView) private val apngView: ApngView = viewRoot.findViewById(R.id.apngView)
init { init {
apngView.timeAnimationStart = _activity.timeAnimationStart apngView.timeAnimationStart = actList.timeAnimationStart
} }
private var lastId: Int = 0 private var lastId: Int = 0

View File

@ -125,7 +125,7 @@ class ActViewer : AsyncActivity() {
Log.d(TAG, "$title[$i] timeWidth=${f.timeWidth}") Log.d(TAG, "$title[$i] timeWidth=${f.timeWidth}")
val bitmap = f.bitmap val bitmap = f.bitmap
FileOutputStream(File(dir, "${title}_${i}.png")).use { fo -> FileOutputStream(File(dir, "${title}_$i.png")).use { fo ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fo) bitmap.compress(Bitmap.CompressFormat.PNG, 100, fo)
} }

View File

@ -10,119 +10,117 @@ import android.view.View
import jp.juggler.apng.ApngFrames import jp.juggler.apng.ApngFrames
import kotlin.math.max import kotlin.math.max
class ApngView : View{ class ApngView : View {
var timeAnimationStart :Long =0L
var apngFrames : ApngFrames? = null
set(value) {
field = value
initializeScale()
}
constructor(context : Context) : super(context, null) {
init(context)
}
constructor(context : Context, attrs : AttributeSet?) : super(context, attrs, 0) {
init(context)
}
constructor(context : Context, attrs : AttributeSet?, defStyle : Int) : super(context, attrs, defStyle) {
init(context)
}
private var wView : Float = 1f
private var hView : Float = 1f
private var aspectView: Float = 1f
private var wImage : Float = 1f
private var hImage : Float = 1f
private var aspectImage : Float = 1f
var timeAnimationStart: Long = 0L
private var currentScale : Float = 1f
private var currentTransX :Float = 0f
private var currentTransY :Float = 0f
private val drawMatrix = Matrix()
private val paint = Paint()
private val findFrameResult = ApngFrames.FindFrameResult()
var apngFrames: ApngFrames? = null
set(value) {
field = value
initializeScale()
}
private fun init(@Suppress("UNUSED_PARAMETER") context:Context){ constructor(context: Context) : super(context, null) {
// init(context)
} }
override fun onSizeChanged(w : Int, h : Int, oldw : Int, oldh : Int) {
super.onSizeChanged(w, h, oldw, oldh)
wView = max(1, w).toFloat()
hView = max(1, h).toFloat()
aspectView = wView / hView
initializeScale()
}
private fun initializeScale(){
val apngFrames = this.apngFrames
if( apngFrames != null) {
wImage = max(1, apngFrames.width).toFloat()
hImage = max(1, apngFrames.height).toFloat()
aspectImage = wImage / hImage
currentScale = if(aspectView > aspectImage) {
hView / hImage
} else {
wView / wImage
}
val wDraw = wImage * currentScale
val hDraw = hImage * currentScale
currentTransX = (wView - wDraw) / 2f
currentTransY = (hView - hDraw) / 2f
} else {
currentScale = 1f
currentTransX = 0f
currentTransY = 0f
}
invalidate()
}
override fun onDraw(canvas : Canvas) { constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, 0) {
super.onDraw(canvas) init(context)
}
val apngFrames = this.apngFrames
if(apngFrames != null ){
val t = SystemClock.elapsedRealtime() - timeAnimationStart
apngFrames.findFrame(findFrameResult,t) constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
val delay = findFrameResult.delay context,
val bitmap = findFrameResult.bitmap attrs,
if( bitmap != null) { defStyle
drawMatrix.reset() ) {
drawMatrix.postScale(currentScale, currentScale) init(context)
drawMatrix.postTranslate(currentTransX, currentTransY) }
paint.isFilterBitmap = currentScale < 4f
canvas.drawBitmap(bitmap, drawMatrix, paint) private var wView: Float = 1f
if( delay != Long.MAX_VALUE){ private var hView: Float = 1f
postInvalidateDelayed(max(1L,delay))
} private var aspectView: Float = 1f
}
} private var wImage: Float = 1f
}
private var hImage: Float = 1f
private var aspectImage: Float = 1f
private var currentScale: Float = 1f
private var currentTransX: Float = 0f
private var currentTransY: Float = 0f
private val drawMatrix = Matrix()
private val paint = Paint()
private val findFrameResult = ApngFrames.FindFrameResult()
private fun init(@Suppress("UNUSED_PARAMETER") context: Context) {
//
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
wView = max(1, w).toFloat()
hView = max(1, h).toFloat()
aspectView = wView / hView
initializeScale()
}
private fun initializeScale() {
val apngFrames = this.apngFrames
if (apngFrames != null) {
wImage = max(1, apngFrames.width).toFloat()
hImage = max(1, apngFrames.height).toFloat()
aspectImage = wImage / hImage
currentScale = if (aspectView > aspectImage) {
hView / hImage
} else {
wView / wImage
}
val wDraw = wImage * currentScale
val hDraw = hImage * currentScale
currentTransX = (wView - wDraw) / 2f
currentTransY = (hView - hDraw) / 2f
} else {
currentScale = 1f
currentTransX = 0f
currentTransY = 0f
}
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val apngFrames = this.apngFrames
if (apngFrames != null) {
val t = SystemClock.elapsedRealtime() - timeAnimationStart
apngFrames.findFrame(findFrameResult, t)
val delay = findFrameResult.delay
val bitmap = findFrameResult.bitmap
if (bitmap != null) {
drawMatrix.reset()
drawMatrix.postScale(currentScale, currentScale)
drawMatrix.postTranslate(currentTransX, currentTransY)
paint.isFilterBitmap = currentScale < 4f
canvas.drawBitmap(bitmap, drawMatrix, paint)
if (delay != Long.MAX_VALUE) {
postInvalidateDelayed(max(1L, delay))
}
}
}
}
} }