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
.idea/androidTestResultsUserPreferences.xml

View File

@ -9,29 +9,28 @@ class ApngAnimationControl(
// This must equal the number of `fcTL` chunks.
// 0 is not a valid value.
// 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 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 {
const val PLAY_INDEFINITELY = 0
internal fun parse(src : ByteSequence) : ApngAnimationControl {
internal fun parse(src: ByteSequence): ApngAnimationControl {
val numFrames = src.readInt32()
val numPlays = src.readInt32()
return ApngAnimationControl(
numFrames = numFrames,
numPlays = numPlays
)
}
}
override fun toString() = "ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"
val isFinite : Boolean
val isFinite: Boolean
get() = numPlays > PLAY_INDEFINITELY
}

View File

@ -4,34 +4,34 @@ package jp.juggler.apng
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 green : Int
val blue : Int
val index : Int
val red: Int
val green: Int
val blue: Int
val index: Int
init {
when(colorType) {
when (colorType) {
ColorType.GREY, ColorType.GREY_ALPHA -> {
val v = src.readUInt16()
red = v
green = v
blue = v
index = - 1
index = -1
}
ColorType.RGB, ColorType.RGBA -> {
red = src.readUInt16()
green = src.readUInt16()
blue = src.readUInt16()
index = - 1
index = -1
}
ColorType.INDEX -> {
red = - 1
green = - 1
blue = - 1
red = -1
green = -1
blue = -1
index = src.readUInt8()
}
}

View File

@ -2,15 +2,15 @@
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
val colors = IntArray(width * height)
// widthとheightを再指定する。ビットマップはそのまま再利用する
fun reset(width : Int, height : Int) {
fun reset(width: Int, height: Int) {
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")
this.width = width
this.height = height
@ -21,42 +21,42 @@ class ApngBitmap(var width : Int, var height : Int) {
// ビットマップ中の位置を保持して、ピクセルへの書き込みと位置の更新を行う
inner class Pointer {
private var pos : Int = 0
var step : Int = 1
private var pos: Int = 0
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
((r and 255) shl 16) or
((g and 255) shl 8) or
(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.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)
val color : Int
val color: Int
get() = colors[pos]
val alpha : Int
val alpha: Int
get() = (colors[pos] shr 24) and 255
val red : Int
val red: Int
get() = (colors[pos] shr 16) and 255
val green : Int
val green: Int
get() = (colors[pos] shr 8) and 255
val blue : Int
val blue: Int
get() = (colors[pos]) and 255
}

View File

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

View File

@ -11,21 +11,21 @@ object ApngDecoder {
private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a)
fun parseStream(
inStream : InputStream,
callback : ApngDecoderCallback
inStream: InputStream,
callback: ApngDecoderCallback,
) {
val apng = Apng()
val tokenizer = StreamTokenizer(inStream)
val pngHeader = tokenizer.readBytes(8)
if(! pngHeader.contentEquals(PNG_SIGNATURE)) {
if (!pngHeader.contentEquals(PNG_SIGNATURE)) {
throw ApngParseError("header not match")
}
var lastSequenceNumber : Int? = null
fun checkSequenceNumber(n : Int) {
var lastSequenceNumber: Int? = null
fun checkSequenceNumber(n: Int) {
val last = lastSequenceNumber
if(last != null && n <= last) {
if (last != null && n <= last) {
throw ApngParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n")
}
lastSequenceNumber = n
@ -33,16 +33,16 @@ object ApngDecoder {
val inBuffer = ByteArray(4096)
val inflateBufferPool = BufferPool(8192)
var idatDecoder : IdatDecoder? = null
var fdatDecoder : IdatDecoder? = null
var idatDecoder: IdatDecoder? = null
var fdatDecoder: IdatDecoder? = null
val crc32 = CRC32()
var lastFctl : ApngFrameControl? = null
var bitmap : ApngBitmap? = null
var lastFctl: ApngFrameControl? = null
var bitmap: ApngBitmap? = null
loop@ while(true) {
loop@ while (true) {
crc32.reset()
val chunk = ApngChunk(crc32, tokenizer)
when(chunk.type) {
when (chunk.type) {
"IEND" -> break@loop
@ -67,7 +67,7 @@ object ApngDecoder {
"tRNS" -> {
val header = apng.header ?: throw ApngParseError("missing IHDR")
val body = chunk.readBody(crc32, tokenizer)
when(header.colorType) {
when (header.colorType) {
ColorType.GREY -> apng.transparentColor =
ApngTransparentColor(true, ByteSequence(body))
ColorType.RGB -> apng.transparentColor =
@ -80,7 +80,7 @@ object ApngDecoder {
"IDAT" -> {
val header = apng.header ?: throw ApngParseError("missing IHDR")
if(idatDecoder == null) {
if (idatDecoder == null) {
bitmap ?: throw ApngParseError("missing bitmap")
bitmap.reset(header.width, header.height)
idatDecoder = IdatDecoder(
@ -91,7 +91,7 @@ object ApngDecoder {
) {
callback.onDefaultImage(apng, bitmap)
val fctl = lastFctl
if(fctl != null) {
if (fctl != null) {
// IDATより前にfcTLが登場しているなら、そのfcTLの画像はIDATと同じ
callback.onAnimationFrame(apng, fctl, bitmap)
}
@ -124,7 +124,7 @@ object ApngDecoder {
"fdAT" -> {
val fctl = lastFctl ?: throw ApngParseError("missing fCTL before fdAT")
if(fdatDecoder == null) {
if (fdatDecoder == null) {
bitmap ?: throw ApngParseError("missing bitmap")
bitmap.reset(fctl.width, fctl.height)
fdatDecoder = IdatDecoder(
@ -153,7 +153,7 @@ object ApngDecoder {
"tIME", // timestamp
"hIST", // histogram
"pHYs", // Physical pixel dimensions
"sPLT" // Suggested palette (おそらく減色用?)
"sPLT", // Suggested palette (おそらく減色用?)
-> chunk.skipBody(tokenizer)
else -> {

View File

@ -3,29 +3,28 @@ package jp.juggler.apng
interface ApngDecoderCallback {
// called for non-fatal warning
fun onApngWarning(message : String)
fun onApngWarning(message: String)
// 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.
fun onHeader(apng : Apng, header : ApngImageHeader)
fun onHeader(apng: Apng, header: ApngImageHeader)
// called when APNG Animation Control is detected.
fun onAnimationInfo(
apng : Apng,
header : ApngImageHeader,
animationControl : ApngAnimationControl
apng: Apng,
header: ApngImageHeader,
animationControl: ApngAnimationControl,
)
// 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.
// its bitmap may same to default image for first frame.
// ( 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,27 +4,27 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence
class ApngFrameControl (
val width : Int,
val height : Int,
val xOffset : Int,
val yOffset : Int,
val disposeOp : DisposeOp,
val blendOp : BlendOp,
val sequenceNumber:Int,
val delayMilliseconds: Long
class ApngFrameControl(
val width: Int,
val height: Int,
val xOffset: Int,
val yOffset: Int,
val disposeOp: DisposeOp,
val blendOp: BlendOp,
val sequenceNumber: Int,
val delayMilliseconds: Long,
) {
companion object{
internal fun parse(src : ByteSequence, sequenceNumber:Int) :ApngFrameControl{
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 }
val delayDen = src.readUInt16().let { if (it == 0) 100 else it }
var num : Int
var num: Int
num = src.readUInt8()
val disposeOp = DisposeOp.values().first { it.num == num }
@ -33,15 +33,15 @@ class ApngFrameControl (
val blendOp = BlendOp.values().first { it.num == num }
return ApngFrameControl(
width =width,
width = width,
height = height,
xOffset = xOffset,
yOffset = yOffset,
disposeOp = disposeOp,
blendOp = blendOp,
sequenceNumber = sequenceNumber,
delayMilliseconds = when(delayDen) {
0,1000 -> delayNum.toLong()
delayMilliseconds = when (delayDen) {
0, 1000 -> delayNum.toLong()
else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong()
}
)
@ -50,5 +50,4 @@ class ApngFrameControl (
override fun toString() =
"ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayMilliseconds=$delayMilliseconds,disposeOp=$disposeOp,blendOp=$blendOp)"
}

View File

@ -6,24 +6,23 @@ import jp.juggler.apng.util.ByteSequence
// information from IHDR chunk.
class ApngImageHeader(
val width : Int,
val height : Int,
val bitDepth : Int,
val colorType : ColorType,
val compressionMethod : CompressionMethod,
val filterMethod : FilterMethod,
val interlaceMethod : InterlaceMethod
val width: Int,
val height: Int,
val bitDepth: Int,
val colorType: ColorType,
val compressionMethod: CompressionMethod,
val filterMethod: FilterMethod,
val interlaceMethod: InterlaceMethod,
) {
companion object{
internal fun parse (src : ByteSequence) :ApngImageHeader{
companion object {
internal fun parse(src: ByteSequence): ApngImageHeader {
val width = 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
var num: Int
//
num = src.readUInt8()
val colorType = ColorType.values().first { it.num == num }
@ -38,14 +37,13 @@ class ApngImageHeader(
val interlaceMethod = InterlaceMethod.values().first { it.num == num }
return ApngImageHeader(
width =width,
width = width,
height = height,
bitDepth = bitDepth,
colorType = colorType,
compressionMethod = compressionMethod,
filterMethod = filterMethod,
interlaceMethod = interlaceMethod
interlaceMethod = interlaceMethod,
)
}
}

View File

@ -6,7 +6,7 @@ import jp.juggler.apng.util.getUInt8
import kotlin.math.min
class ApngPalette(
src : ByteArray // repeat of R,G,B
src: ByteArray, // repeat of R,G,B
) {
companion object {
@ -14,15 +14,15 @@ class ApngPalette(
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 {
val entryCount = src.size / 3
list = IntArray(entryCount)
var pos = 0
for(i in 0 until entryCount) {
for (i in 0 until entryCount) {
list[i] = OPAQUE or
(src.getUInt8(pos) shl 16) or
(src.getUInt8(pos + 1) shl 8) or
@ -34,9 +34,9 @@ class ApngPalette(
override fun toString() = "palette(${list.size} entries,hasAlpha=$hasAlpha)"
// update alpha value from tRNS chunk data
fun parseTRNS(ba : ByteArray) {
fun parseTRNS(ba: ByteArray) {
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)
}
}

View File

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

View File

@ -1,7 +1,6 @@
package jp.juggler.apng
import java.io.InputStream
import java.lang.StringBuilder
import kotlin.math.min
// https://raw.githubusercontent.com/rtyley/animated-gif-lib-for-java/master/src/main/java/com/madgag/gif/fmsware/GifDecoder.java
@ -11,11 +10,11 @@ import kotlin.math.min
// http://www.theimage.com/animation/pages/disposal3.html
// great sample images.
class GifDecoder(val callback : GifDecoderCallback) {
class GifDecoder(val callback: GifDecoderCallback) {
private class Rectangle(var x : Int = 0, var y : Int = 0, var w : Int = 0, var h : Int = 0) {
private class Rectangle(var x: Int = 0, var y: Int = 0, var w: Int = 0, var h: Int = 0) {
fun set(src : Rectangle) {
fun set(src: Rectangle) {
this.x = src.x
this.y = src.y
this.w = src.w
@ -23,37 +22,37 @@ class GifDecoder(val callback : GifDecoderCallback) {
}
}
private class Reader(val bis : InputStream) {
private class Reader(val bis: InputStream) {
var block = ByteArray(256) // current data block
var blockSize = 0 // block size
// Reads a single byte from the input stream.
fun byte() : Int = bis.read()
fun byte(): Int = bis.read()
// Reads next 16-bit value, LSB first
fun UInt16() = byte() or (byte() shl 8)
fun uInt16() = byte() or (byte() shl 8)
fun array(ba : ByteArray, offset : Int = 0, length : Int = ba.size - offset) {
fun array(ba: ByteArray, offset: Int = 0, length: Int = ba.size - offset) {
var nRead = 0
while(nRead < length) {
while (nRead < length) {
val delta = bis.read(ba, offset + nRead, length - nRead)
if(delta == - 1) error("unexpected End of Stream")
if (delta == -1) error("unexpected End of Stream")
nRead += delta
}
}
// Reads specified bytes and compose it to ascii string
fun string(n : Int) : String {
return StringBuilder(n).apply{
fun string(n: Int): String {
return StringBuilder(n).apply {
ByteArray(n)
.also{ array(it)}
.forEach { append( Char( it.toInt() and 255)) }
.also { array(it) }
.forEach { append(Char(it.toInt() and 255)) }
}.toString()
}
// Reads next variable length block
fun block() : ByteArray {
fun block(): ByteArray {
blockSize = byte()
array(block, 0, blockSize)
return block
@ -63,12 +62,12 @@ class GifDecoder(val callback : GifDecoderCallback) {
fun skipBlock() {
do {
block()
} while(blockSize > 0)
} while (blockSize > 0)
}
}
// 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
enum class Dispose(val num : Int) {
enum class Dispose(val num: Int) {
Unspecified(0),
DontDispose(1),
@ -78,7 +77,7 @@ class GifDecoder(val callback : GifDecoderCallback) {
companion object {
private const val MaxStackSize = 4096
private const val NullCode = - 1
private const val NullCode = -1
private const val b0 = 0.toByte()
private const val OPAQUE = 255 shl 24
}
@ -88,7 +87,7 @@ class GifDecoder(val callback : GifDecoderCallback) {
private var gctSize = 0 // size of global color table
private var loopCount = 1 // iterations; 0 = repeat forever
private var gct : IntArray? = null // global color table
private var gct: IntArray? = null // global color table
private var bgIndex = 0 // background color index
private var bgColor = 0 // background color
@ -111,45 +110,45 @@ class GifDecoder(val callback : GifDecoderCallback) {
private var transIndex = 0 // transparent color index
// LZW decoder working arrays
private var prefix : ShortArray? = null
private var suffix : ByteArray? = null
private var pixelStack : ByteArray? = null
private var pixels : ByteArray? = null
private var prefix: ShortArray? = null
private var suffix: ByteArray? = null
private var pixelStack: ByteArray? = null
private var pixels: ByteArray? = null
private val frames = ArrayList<Pair<ApngFrameControl, ApngBitmap>>()
private var previousImage : ApngBitmap? = null
private var previousImage: ApngBitmap? = null
// 現在のdispose指定と描画結果を覚えておく
private fun memoryLastDispose(image : ApngBitmap) {
if(dispose != Dispose.RestorePrevious) previousImage = image
private fun memoryLastDispose(image: ApngBitmap) {
if (dispose != Dispose.RestorePrevious) previousImage = image
lastDispose = dispose
lastRect.set(srcRect)
lastBgColor = bgColor
}
// 前回のdispose指定を反映する
private fun applyLastDispose(destImage : ApngBitmap) {
private fun applyLastDispose(destImage: ApngBitmap) {
if(lastDispose == Dispose.Unspecified) return
if (lastDispose == Dispose.Unspecified) return
// restore previous image
val previousImage = this.previousImage
if(previousImage != null) {
if (previousImage != null) {
System.arraycopy(previousImage.colors, 0, destImage.colors, 0, destImage.colors.size)
}
if(lastDispose == Dispose.RestoreBackground) {
if (lastDispose == Dispose.RestoreBackground) {
// fill lastRect
val fillColor = if(transparency) {
val fillColor = if (transparency) {
0 // assume background is transparent
} else {
lastBgColor // use given background color
}
for(y in lastRect.y until lastRect.y + lastRect.h) {
for (y in lastRect.y until lastRect.y + lastRect.h) {
val fillStart = y * destImage.width + lastRect.x
val fillWidth = lastRect.w
destImage.colors.fill(
@ -163,39 +162,37 @@ class GifDecoder(val callback : GifDecoderCallback) {
// render to ApngBitmap
// may use some previous frame.
private fun render(destImage : ApngBitmap, act : IntArray) {
private fun render(destImage: ApngBitmap, act: IntArray) {
// expose destination image's pixels as int array
val dest = destImage.colors
// copy each source line to the appropriate place in the destination
var pass = 1
var inc = 8
var iline = 0
for(i in 0 until srcRect.h) {
var iLine = 0
for (i in 0 until srcRect.h) {
var line = i
if(interlace) {
if(iline >= srcRect.h) {
when(++ pass) {
if (interlace) {
if (iLine >= srcRect.h) {
when (++pass) {
2 -> {
iline = 4
iLine = 4
}
3 -> {
iline = 2
iLine = 2
inc = 4
}
4 -> {
iline = 1
iLine = 1
inc = 2
}
}
}
line = iline
iline += inc
line = iLine
iLine += inc
}
line += srcRect.y
if(line < height) {
if (line < height) {
// start of line in source
var sx = i * srcRect.w
@ -204,46 +201,45 @@ class GifDecoder(val callback : GifDecoderCallback) {
val k = line * width
// loop for dest line.
for(dx in k + srcRect.x until min(k + width, k + srcRect.x + srcRect.w)) {
for (dx in k + srcRect.x until min(k + width, k + srcRect.x + srcRect.w)) {
// map color and insert in destination
val index = pixels !![sx ++].toInt() and 0xff
val index = pixels!![sx++].toInt() and 0xff
val c = act[index]
if(c != 0) dest[dx] = c
if (c != 0) dest[dx] = c
}
}
}
}
/**
* Decodes LZW image data into pixel array.
* Adapted from John Cristy's ImageMagick.
*/
private fun decodeImageData(reader : Reader) {
private fun decodeImageData(reader: Reader) {
// allocate pixel array if need
val nPixels = srcRect.w * srcRect.h
if((pixels?.size ?: 0) < nPixels) pixels = ByteArray(nPixels)
val pixels = this.pixels !!
if ((pixels?.size ?: 0) < nPixels) pixels = ByteArray(nPixels)
val pixels = this.pixels!!
if(prefix == null) prefix = ShortArray(MaxStackSize)
if(suffix == null) suffix = ByteArray(MaxStackSize)
if(pixelStack == null) pixelStack = ByteArray(MaxStackSize + 1)
val prefix = this.prefix !!
val suffix = this.suffix !!
val pixelStack = this.pixelStack !!
if (prefix == null) prefix = ShortArray(MaxStackSize)
if (suffix == null) suffix = ByteArray(MaxStackSize)
if (pixelStack == null) pixelStack = ByteArray(MaxStackSize + 1)
val prefix = this.prefix!!
val suffix = this.suffix!!
val pixelStack = this.pixelStack!!
// Initialize GIF data stream decoder.
val data_size = reader.byte()
val clear = 1 shl data_size
val end_of_information = clear + 1
val dataSize = reader.byte()
val clear = 1 shl dataSize
val endOfInformation = clear + 1
var available = clear + 2
var old_code = NullCode
var code_size = data_size + 1
var code_mask = (1 shl code_size) - 1
var oldCode = NullCode
var codeSize = dataSize + 1
var codeMask = (1 shl codeSize) - 1
for(code in 0 until clear) {
for (code in 0 until clear) {
prefix[code] = 0
suffix[code] = code.toByte()
}
@ -258,85 +254,85 @@ class GifDecoder(val callback : GifDecoderCallback) {
var pi = 0
var i = 0
while(i < nPixels) {
if(top == 0) {
if(bits < code_size) {
while (i < nPixels) {
if (top == 0) {
if (bits < codeSize) {
// Load bytes until there are enough bits for a code.
if(count == 0) {
if (count == 0) {
// Read a new data block.
reader.block()
count = reader.blockSize
if(count <= 0) break
if (count <= 0) break
bi = 0
}
datum += (reader.block[bi].toInt() and 0xff) shl bits
bits += 8
bi ++
count --
bi++
count--
continue
}
// Get the next code.
var code = datum and code_mask
datum = datum shr code_size
bits -= code_size
var code = datum and codeMask
datum = datum shr codeSize
bits -= codeSize
// Interpret the code
if((code > available) || (code == end_of_information)) break
if ((code > available) || (code == endOfInformation)) break
if(code == clear) {
if (code == clear) {
// Reset decoder.
code_size = data_size + 1
code_mask = (1 shl code_size) - 1
codeSize = dataSize + 1
codeMask = (1 shl codeSize) - 1
available = clear + 2
old_code = NullCode
oldCode = NullCode
continue
}
if(old_code == NullCode) {
pixelStack[top ++] = suffix[code]
old_code = code
if (oldCode == NullCode) {
pixelStack[top++] = suffix[code]
oldCode = code
first = code
continue
}
val in_code = code
val inCode = code
if(code == available) {
pixelStack[top ++] = first.toByte()
code = old_code
if (code == available) {
pixelStack[top++] = first.toByte()
code = oldCode
}
while(code > clear) {
pixelStack[top ++] = suffix[code]
while (code > clear) {
pixelStack[top++] = suffix[code]
code = prefix[code].toInt()
}
first = suffix[code].toInt() and 0xff
// Add a new string to the string table,
if(available >= MaxStackSize) {
pixelStack[top ++] = first.toByte()
if (available >= MaxStackSize) {
pixelStack[top++] = first.toByte()
continue
}
pixelStack[top ++] = first.toByte()
prefix[available] = old_code.toShort()
pixelStack[top++] = first.toByte()
prefix[available] = oldCode.toShort()
suffix[available] = first.toByte()
available ++
available++
if((available and code_mask) == 0 && available < MaxStackSize) {
code_size ++
code_mask += available
if ((available and codeMask) == 0 && available < MaxStackSize) {
codeSize++
codeMask += available
}
old_code = in_code
oldCode = inCode
}
// Pop a pixel off the pixel stack.
top --
pixels[pi ++] = pixelStack[top]
i ++
top--
pixels[pi++] = pixelStack[top]
i++
}
// clear missing pixels
for(n in pi until nPixels) {
for (n in pi until nPixels) {
pixels[n] = b0
}
}
@ -347,7 +343,7 @@ class GifDecoder(val callback : GifDecoderCallback) {
* @param nColors int number of colors to read
* @return int array containing 256 colors (packed ARGB with full alpha)
*/
private fun parseColorTable(reader : Reader, nColors : Int) : IntArray {
private fun parseColorTable(reader: Reader, nColors: Int): IntArray {
val nBytes = 3 * nColors
val c = ByteArray(nBytes)
reader.array(c)
@ -356,33 +352,33 @@ class GifDecoder(val callback : GifDecoderCallback) {
val tab = IntArray(256)
var i = 0
var j = 0
while(i < nColors) {
while (i < nColors) {
val r = c[j].toInt() and 255
val g = c[j + 1].toInt() and 255
val b = c[j + 2].toInt() and 255
j += 3
tab[i ++] = (OPAQUE or (r shl 16) or (g shl 8) or b)
tab[i++] = (OPAQUE or (r shl 16) or (g shl 8) or b)
}
return tab
}
private fun parseDispose(num : Int) =
private fun parseDispose(num: Int) =
Dispose.values().find { it.num == num } ?: error("unknown dispose $num")
/**
* Reads Graphics Control Extension values
*/
private fun parseGraphicControlExt(reader : Reader) {
private fun parseGraphicControlExt(reader: Reader) {
reader.byte() // block size
val packed = reader.byte() // packed fields
dispose = parseDispose((packed and 0x1c) shr 2) // disposal method
if(callback.canGifDebug()) callback.onGifDebug("parseGraphicControlExt: frame=${frames.size} dispose=$dispose")
if (callback.canGifDebug()) callback.onGifDebug("parseGraphicControlExt: frame=${frames.size} dispose=$dispose")
// elect to keep old image if discretionary
if(dispose == Dispose.Unspecified) dispose = Dispose.DontDispose
if (dispose == Dispose.Unspecified) dispose = Dispose.DontDispose
transparency = (packed and 1) != 0
// delay in milliseconds
delay = reader.UInt16() * 10
delay = reader.uInt16() * 10
// transparent color index
transIndex = reader.byte()
// block terminator
@ -390,25 +386,25 @@ class GifDecoder(val callback : GifDecoderCallback) {
}
// Reads Netscape extension to obtain iteration count
private fun readNetscapeExt(reader : Reader) {
private fun readNetscapeExt(reader: Reader) {
do {
val block = reader.block()
if(block[0].toInt() == 1) {
if (block[0].toInt() == 1) {
// loop count sub-block
val b1 = block[1].toInt() and 255
val b2 = block[2].toInt() and 255
loopCount = ((b2 shl 8) and b1)
}
} while(reader.blockSize > 0)
} while (reader.blockSize > 0)
}
// Reads next frame image
private fun parseFrame(reader : Reader) {
private fun parseFrame(reader: Reader) {
// (sub)image position & size
srcRect.x = reader.UInt16()
srcRect.y = reader.UInt16()
srcRect.w = reader.UInt16()
srcRect.h = reader.UInt16()
srcRect.x = reader.uInt16()
srcRect.y = reader.uInt16()
srcRect.w = reader.uInt16()
srcRect.h = reader.uInt16()
val packed = reader.byte()
lctFlag = (packed and 0x80) != 0 // 1 - local color table flag
@ -417,17 +413,17 @@ class GifDecoder(val callback : GifDecoderCallback) {
// 4-5 - reserved
lctSize = 2 shl (packed and 7) // 6-8 - local color table size
val act = if(lctFlag) {
val act = if (lctFlag) {
// make local table active
parseColorTable(reader, lctSize)
} else {
// make global table active
if(bgIndex == transIndex) bgColor = 0
gct !!
if (bgIndex == transIndex) bgColor = 0
gct!!
}
var save = 0
if(transparency) {
if (transparency) {
save = act[transIndex]
act[transIndex] = 0 // set transparent color if specified
}
@ -456,7 +452,7 @@ class GifDecoder(val callback : GifDecoderCallback) {
)
)
if(transparency) {
if (transparency) {
act[transIndex] = save
}
@ -469,23 +465,23 @@ class GifDecoder(val callback : GifDecoderCallback) {
}
// read GIF content blocks
private fun readContents(reader : Reader) : ApngAnimationControl {
loopBlocks@ while(true) {
when(val blockCode = reader.byte()) {
private fun readContents(reader: Reader): ApngAnimationControl {
loopBlocks@ while (true) {
when (val blockCode = reader.byte()) {
// image separator
0x2C -> parseFrame(reader)
// extension
0x21 -> when(reader.byte()) {
0x21 -> when (reader.byte()) {
// graphics control extension
0xf9 -> parseGraphicControlExt(reader)
// application extension
0xff -> {
val block = reader.block()
val app = StringBuilder(12)
for(i in 0 until 11) {
app.append( Char( block[i].toInt() and 255 ))
for (i in 0 until 11) {
app.append(Char(block[i].toInt() and 255))
}
if(app.toString() == "NETSCAPE2.0") {
if (app.toString() == "NETSCAPE2.0") {
readNetscapeExt(reader)
} else {
reader.skipBlock() // don't care
@ -529,10 +525,10 @@ class GifDecoder(val callback : GifDecoderCallback) {
/**
* Reads GIF file header information.
*/
private fun parseImageHeader(reader : Reader) : ApngImageHeader {
private fun parseImageHeader(reader: Reader): ApngImageHeader {
val id = reader.string(6)
if(! id.startsWith("GIF"))
if (!id.startsWith("GIF"))
error("file header not match to GIF.")
/**
@ -540,9 +536,9 @@ class GifDecoder(val callback : GifDecoderCallback) {
*/
// logical screen size
width = reader.UInt16()
height = reader.UInt16()
if(width < 1 || height < 1) error("too small size. ${width}*${height}")
width = reader.uInt16()
height = reader.uInt16()
if (width < 1 || height < 1) error("too small size. $width*$height")
// packed fields
val packed = reader.byte()
@ -556,7 +552,7 @@ class GifDecoder(val callback : GifDecoderCallback) {
bgIndex = reader.byte() // background color index
pixelAspect = reader.byte() // pixel aspect ratio
gct = if(gctFlag) {
gct = if (gctFlag) {
val table = parseColorTable(reader, gctSize)
bgColor = table[bgIndex]
table
@ -576,7 +572,7 @@ class GifDecoder(val callback : GifDecoderCallback) {
)
}
fun parse(src : InputStream) {
fun parse(src: InputStream) {
reset()
@ -586,10 +582,10 @@ class GifDecoder(val callback : GifDecoderCallback) {
// GIFは最後まで読まないとフレーム数が分からない
if(frames.isEmpty()) error("there is no frame.")
if (frames.isEmpty()) error("there is no frame.")
callback.onGifHeader(header)
callback.onGifAnimationInfo(header, animationControl)
for(frame in frames) {
for (frame in frames) {
callback.onGifAnimationFrame(frame.first, frame.second)
}

View File

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

View File

@ -14,7 +14,7 @@ internal class IdatDecoder(
private val bitmap: ApngBitmap,
private val inflateBufferPool: BufferPool,
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)
@ -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 remain = pass_w
var remain = passW
while (remain >= 8) {
remain -= 8
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 remain = pass_w
var remain = passW
while (remain >= 4) {
remain -= 4
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 remain = pass_w
var remain = passW
while (remain >= 2) {
remain -= 2
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 remain = pass_w
var remain = passW
while (remain-- > 0) {
block(baLine.getUInt8(pos++))
}
}
}
private val inflater = Inflater()
@ -424,7 +423,6 @@ internal class IdatDecoder(
// 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}")
// }
}
}
@ -464,7 +462,6 @@ internal class IdatDecoder(
// 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)}")
// }
}
}
}
@ -498,7 +495,7 @@ internal class IdatDecoder(
inStream: InputStream,
size: Int,
inBuffer: ByteArray,
crc32: CRC32
crc32: CRC32,
) {
var foundEnd = false
var inRemain = size
@ -541,8 +538,8 @@ internal class IdatDecoder(
inflateBufferQueue.add(ByteSequence(buffer, 0, nInflated))
// キューに追加したデータをScanLine単位で消費する
@Suppress("ControlFlowWithEmptyBody")
while (!isCompleted && readScanLine()){
@Suppress("ControlFlowWithEmptyBody", "EmptyWhileBlock")
while (!isCompleted && readScanLine()) {
}
if (isCompleted) {

View File

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

View File

@ -2,25 +2,25 @@ package jp.juggler.apng.util
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 + 2) shl 8) or
getUInt8(pos + 3)
internal class ByteSequence(
val array : ByteArray,
var offset : Int,
var length : Int
val array: ByteArray,
var offset: 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 {
if(length < dataSize) throw ApngParseError("readX: unexpected end")
private inline fun <T> readX(dataSize: Int, block: () -> T): T {
if (length < dataSize) throw ApngParseError("readX: unexpected end")
val v = block()
offset += dataSize
length -= dataSize

View File

@ -3,23 +3,23 @@ package jp.juggler.apng.util
import java.util.*
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>()
val remain : Int
val remain: Int
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 readBytes(dst : ByteArray, offset : Int, length : Int) : Int {
fun readBytes(dst: ByteArray, offset: Int, length: Int): Int {
var dstOffset = offset
var dstRemain = length
while(dstRemain > 0 && list.isNotEmpty()) {
while (dstRemain > 0 && list.isNotEmpty()) {
val item = list.first()
if(item.length <= 0) {
if (item.length <= 0) {
bufferRecycler(item)
list.removeFirst()
} else {

View File

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

View File

@ -1,4 +1,5 @@
@file:Suppress("LocalVariableName")
package jp.juggler.apng
import org.junit.Assert.*
@ -7,6 +8,7 @@ import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
@Suppress("LargeClass")
class TestApng {
companion object {
@ -3088,7 +3090,7 @@ class TestApng {
)
}
private fun getResourceFile(path : String) : File {
private fun getResourceFile(path: String): File {
return File(this.javaClass.classLoader.getResource(path).path)
}
@ -3096,7 +3098,7 @@ class TestApng {
fun testByteMod() {
// Int.toByte() は Int値の下位8ビットをByte型で表現する。クリッピングされたりはしない
// ただしKotlinもJavaと同じでbyteは符号付きしかないので面倒くさい…
for(i in - 256 .. 256) {
for (i in -256..256) {
val b = i.toByte()
val i2 = b.toInt() and 255
assertEquals(i and 255, i2)
@ -3109,38 +3111,38 @@ class TestApng {
ApngDecoder.parseStream(
BufferedInputStream(inStream),
object : ApngDecoderCallback {
override fun onApngWarning(message : String) {
override fun onApngWarning(message: String) {
println(message)
}
override fun onApngDebug(message : String) {
override fun onApngDebug(message: String) {
println(message)
}
override fun canApngDebug() : Boolean {
override fun canApngDebug(): Boolean {
return true
}
override fun onAnimationInfo(
apng : Apng,
header : ApngImageHeader,
animationControl : ApngAnimationControl
apng: Apng,
header: ApngImageHeader,
animationControl: ApngAnimationControl,
) {
println("animationControl=$animationControl")
}
override fun onHeader(apng : Apng, header : ApngImageHeader) {
override fun onHeader(apng: Apng, header: ApngImageHeader) {
println("header=$header")
}
override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) {
override fun onDefaultImage(apng: Apng, bitmap: ApngBitmap) {
println("onDefaultImage w=${bitmap.width},h=${bitmap.height}")
}
override fun onAnimationFrame(
apng : Apng,
frameControl : ApngFrameControl,
bitmap : ApngBitmap
apng: Apng,
frameControl: ApngFrameControl,
bitmap: ApngBitmap,
) {
println("onAnimationFrame frameControl=$frameControl")
println("onAnimationFrame w=${bitmap.width},h=${bitmap.height}")
@ -3157,38 +3159,38 @@ class TestApng {
ApngDecoder.parseStream(
BufferedInputStream(inStream),
object : ApngDecoderCallback {
override fun onApngWarning(message : String) {
override fun onApngWarning(message: String) {
println(message)
}
override fun onApngDebug(message : String) {
override fun onApngDebug(message: String) {
println(message)
}
override fun canApngDebug() : Boolean {
override fun canApngDebug(): Boolean {
return true
}
override fun onAnimationInfo(
apng : Apng,
header : ApngImageHeader,
animationControl : ApngAnimationControl
apng: Apng,
header: ApngImageHeader,
animationControl: ApngAnimationControl,
) {
println("animationControl=$animationControl")
}
override fun onHeader(apng : Apng, header : ApngImageHeader) {
override fun onHeader(apng: Apng, header: ApngImageHeader) {
println("header=$header")
}
override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) {
override fun onDefaultImage(apng: Apng, bitmap: ApngBitmap) {
println("onDefaultImage w=${bitmap.width},h=${bitmap.height}")
val w = bitmap.width
val h = bitmap.height
val dstPos = bitmap.pointer()
for(y in 0 until h) {
for(x in 0 until w) {
for (y in 0 until h) {
for (x in 0 until w) {
val srcPos = (x + y * w) * 3
val src_r = imageData[srcPos + 0]
val src_g = imageData[srcPos + 1]
@ -3217,9 +3219,9 @@ class TestApng {
}
override fun onAnimationFrame(
apng : Apng,
frameControl : ApngFrameControl,
bitmap : ApngBitmap
apng: Apng,
frameControl: ApngFrameControl,
bitmap: ApngBitmap,
) {
println("onAnimationFrame frameControl=$frameControl")
println("onAnimationFrame w=${bitmap.width},h=${bitmap.height}")

View File

@ -1,21 +1,14 @@
package jp.juggler.apng
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.graphics.*
import android.util.Log
import java.io.InputStream
import java.util.ArrayList
import kotlin.math.max
import kotlin.math.min
class ApngFrames private constructor(
private val pixelSizeMax : Int = 0,
private val debug : Boolean = false
private val pixelSizeMax: Int = 0,
private val debug: Boolean = false,
) : ApngDecoderCallback, GifDecoderCallback {
companion object {
@ -35,36 +28,36 @@ class ApngFrames private constructor(
color = 0
}
private fun createBlankBitmap(w : Int, h : Int) =
private fun createBlankBitmap(w: Int, h: Int) =
Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
private fun scale(max : Int, num : Int, den : Int) =
private fun scale(max: Int, num: Int, den: Int) =
(max.toFloat() * num.toFloat() / den.toFloat() + 0.5f).toInt()
private fun scaleBitmap(
size_max : Int,
src : Bitmap,
recycleSrc : Boolean = true // true: ownership of "src" will be moved or recycled.
) : Bitmap {
sizeMax: Int,
src: Bitmap,
recycleSrc: Boolean = true, // true: ownership of "src" will be moved or recycled.
): Bitmap {
val wSrc = src.width
val hSrc = src.height
if(size_max <= 0 || (wSrc <= size_max && hSrc <= size_max)) {
return if(recycleSrc) {
if (sizeMax <= 0 || (wSrc <= sizeMax && hSrc <= sizeMax)) {
return if (recycleSrc) {
src
} else {
src.copy(Bitmap.Config.ARGB_8888, false)
}
}
val wDst : Int
val hDst : Int
if(wSrc >= hSrc) {
wDst = size_max
hDst = max(1, scale(size_max, hSrc, wSrc))
val wDst: Int
val hDst: Int
if (wSrc >= hSrc) {
wDst = sizeMax
hDst = max(1, scale(sizeMax, hSrc, wSrc))
} else {
hDst = size_max
wDst = max(1, scale(size_max, wSrc, hSrc))
hDst = sizeMax
wDst = max(1, scale(sizeMax, wSrc, hSrc))
}
//Log.v(TAG,"scaleBitmap: $wSrc,$hSrc => $wDst,$hDst")
@ -74,12 +67,12 @@ class ApngFrames private constructor(
val rectDst = Rect(0, 0, wDst, hDst)
canvas.drawBitmap(src, rectSrc, rectDst, sPaintDontBlend)
if(recycleSrc) src.recycle()
if (recycleSrc) src.recycle()
return b2
}
private fun toAndroidBitmap(src : ApngBitmap) =
private fun toAndroidBitmap(src: ApngBitmap) =
Bitmap.createBitmap(
src.colors, // int[] 配列
0, // offset
@ -89,68 +82,71 @@ class ApngFrames private constructor(
Bitmap.Config.ARGB_8888
)
private fun toAndroidBitmap(src : ApngBitmap, size_max : Int) =
scaleBitmap(size_max, toAndroidBitmap(src))
private fun toAndroidBitmap(src: ApngBitmap, sizeMax: Int) =
scaleBitmap(sizeMax, toAndroidBitmap(src))
private fun parseApng(
inStream : InputStream,
pixelSizeMax : Int,
debug : Boolean = false
) : ApngFrames {
inStream: InputStream,
pixelSizeMax: Int,
debug: Boolean = false,
): ApngFrames {
val result = ApngFrames(pixelSizeMax, debug)
try {
ApngDecoder.parseStream(inStream, result)
result.onParseComplete()
return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
?: throw RuntimeException("APNG has no image")
} catch(ex : Throwable) {
?: error("APNG has no image")
} catch (ex: Throwable) {
result.dispose()
throw ex
}
}
private fun parseGif(
inStream : InputStream,
pixelSizeMax : Int,
debug : Boolean = false
) : ApngFrames {
inStream: InputStream,
pixelSizeMax: Int,
debug: Boolean = false,
): ApngFrames {
val result = ApngFrames(pixelSizeMax, debug)
try {
GifDecoder(result).parse(inStream)
result.onParseComplete()
return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
?: throw RuntimeException("GIF has no image")
} catch(ex : Throwable) {
?: error("GIF has no image")
} catch (ex: Throwable) {
result.dispose()
throw ex
}
}
private val apngHeadKey = byteArrayOf(0x89.toByte(),0x50)
private val apngHeadKey = byteArrayOf(0x89.toByte(), 0x50)
private val gifHeadKey = "GIF".toByteArray(Charsets.UTF_8)
private fun matchBytes(ba1:ByteArray,ba2:ByteArray,length:Int=min(ba1.size,ba2.size)):Boolean{
for( i in 0 until length){
if( ba1[i] != ba2[i] ) return false
private fun matchBytes(
ba1: ByteArray,
ba2: ByteArray,
length: Int = min(ba1.size, ba2.size),
): Boolean {
for (i in 0 until length) {
if (ba1[i] != ba2[i]) return false
}
return true
}
fun parse(
pixelSizeMax : Int,
debug : Boolean = false,
opener : () -> InputStream?
) : ApngFrames? {
pixelSizeMax: Int,
debug: Boolean = false,
opener: () -> InputStream?,
): ApngFrames? {
val buf = ByteArray(8) { 0.toByte() }
opener()?.use { it.read(buf, 0, buf.size) }
if(buf.size >= 8 && matchBytes(buf, apngHeadKey) ) {
if (buf.size >= 8 && matchBytes(buf, apngHeadKey)) {
return opener()?.use { parseApng(it, pixelSizeMax, debug) }
}
if(buf.size >= 6 && matchBytes(buf, gifHeadKey) ) {
if (buf.size >= 6 && matchBytes(buf, gifHeadKey)) {
return opener()?.use { parseGif(it, pixelSizeMax, debug) }
}
@ -158,52 +154,52 @@ class ApngFrames private constructor(
}
}
private var header : ApngImageHeader? = null
private var animationControl : ApngAnimationControl? = null
private var header: ApngImageHeader? = null
private var animationControl: ApngAnimationControl? = null
// width,height (after resized)
var width : Int = 1
var width: Int = 1
private set
var height : Int = 1
var height: Int = 1
private set
class Frame(
val bitmap : Bitmap,
val timeStart : Long,
val timeWidth : Long
val bitmap: Bitmap,
val timeStart: Long,
val timeWidth: Long,
)
var frames : ArrayList<Frame>? = null
var frames: ArrayList<Frame>? = null
@Suppress("MemberVisibilityCanBePrivate")
val numFrames : Int
val numFrames: Int
get() = frames?.size ?: 1
@Suppress("unused")
val hasMultipleFrame : Boolean
val hasMultipleFrame: Boolean
get() = numFrames > 1
private var timeTotal = 0L
private lateinit var canvas : Canvas
private lateinit var canvas: Canvas
private var canvasBitmap : Bitmap? = null
private var canvasBitmap: Bitmap? = null
// 再生速度の調整
private var durationScale = 1f
// APNGじゃなかった場合に使われる
private var defaultImage : Bitmap? = null
private var defaultImage: Bitmap? = null
set(value) {
field = value
if(value != null) {
if (value != null) {
width = value.width
height = value.height
}
}
constructor(bitmap : Bitmap) : this() {
constructor(bitmap: Bitmap) : this() {
defaultImage = bitmap
}
@ -212,11 +208,11 @@ class ApngFrames private constructor(
canvasBitmap = null
val frames = this.frames
if(frames != null) {
if(frames.size > 1) {
if (frames != null) {
if (frames.size > 1) {
defaultImage?.recycle()
defaultImage = null
} else if(frames.size == 1) {
} else if (frames.size == 1) {
defaultImage?.recycle()
defaultImage = frames.first().bitmap
frames.clear()
@ -231,15 +227,15 @@ class ApngFrames private constructor(
}
class FindFrameResult {
var bitmap : Bitmap? = null // may null
var delay : Long = 0 // 再描画が必要ない場合は Long.MAX_VALUE
var bitmap: Bitmap? = null // may null
var delay: Long = 0 // 再描画が必要ない場合は Long.MAX_VALUE
}
// シーク位置に応じたコマ画像と次のコマまでの残り時間をresultに格納する
@Suppress("unused")
fun findFrame(result : FindFrameResult, t : Long) {
fun findFrame(result: FindFrameResult, t: Long) {
if(defaultImage != null) {
if (defaultImage != null) {
result.bitmap = defaultImage
result.delay = Long.MAX_VALUE
return
@ -248,7 +244,7 @@ class ApngFrames private constructor(
val animationControl = this.animationControl
val frames = this.frames
if(animationControl == null || frames == null || frames.isEmpty()) {
if (animationControl == null || frames == null || frames.isEmpty()) {
// ここは通らないはず…
result.bitmap = null
result.delay = Long.MAX_VALUE
@ -258,8 +254,8 @@ class ApngFrames private constructor(
val frameCount = frames.size
val isFinite = animationControl.isFinite
val repeatSequenceCount = if(isFinite) animationControl.numPlays else 1
val endWait = if(isFinite) DELAY_AFTER_END else 0L
val repeatSequenceCount = if (isFinite) animationControl.numPlays else 1
val endWait = if (isFinite) DELAY_AFTER_END else 0L
val timeTotalLoop = max(1, timeTotal * repeatSequenceCount + endWait)
val tf = (max(0, t) / durationScale).toLong()
@ -267,7 +263,7 @@ class ApngFrames private constructor(
// 全体の繰り返し時刻で余りを計算
val tl = tf % timeTotalLoop
if(tl >= timeTotalLoop - endWait) {
if (tl >= timeTotalLoop - endWait) {
// 終端で待機状態
result.bitmap = frames[frameCount - 1].bitmap
result.delay = (0.5f + (timeTotalLoop - tl) * durationScale).toLong()
@ -280,20 +276,20 @@ class ApngFrames private constructor(
// フレームリストを時刻で二分探索
var s = 0
var e = frameCount
while(e - s > 1) {
while (e - s > 1) {
val mid = s + e shr 1
val frame = frames[mid]
// log.d("s=%d,m=%d,e=%d tt=%d,fs=%s,fe=%d",s,mid,e,tt,frame.timeStart,frame.timeStart+frame.timeWidth );
if(tt < frame.timeStart) {
if (tt < frame.timeStart) {
e = mid
} else if(tt >= frame.timeStart + frame.timeWidth) {
} else if (tt >= frame.timeStart + frame.timeWidth) {
s = mid + 1
} else {
s = mid
break
}
}
s = if(s < 0) 0 else if(s >= frameCount - 1) frameCount - 1 else s
s = if (s < 0) 0 else if (s >= frameCount - 1) frameCount - 1 else s
val frame = frames[s]
val delay = frame.timeStart + frame.timeWidth - tt
result.bitmap = frames[s].bitmap
@ -305,26 +301,26 @@ class ApngFrames private constructor(
/////////////////////////////////////////////////////
// implements ApngDecoderCallback
override fun onApngWarning(message : String) {
override fun onApngWarning(message: String) {
Log.w(TAG, message)
}
override fun onApngDebug(message : String) {
override fun onApngDebug(message: String) {
Log.d(TAG, message)
}
override fun canApngDebug() : Boolean = debug
override fun canApngDebug(): Boolean = debug
override fun onHeader(apng : Apng, header : ApngImageHeader) {
override fun onHeader(apng: Apng, header: ApngImageHeader) {
this.header = header
}
override fun onAnimationInfo(
apng : Apng,
header : ApngImageHeader,
animationControl : ApngAnimationControl
apng: Apng,
header: ApngImageHeader,
animationControl: ApngAnimationControl,
) {
if(debug) {
if (debug) {
Log.d(TAG, "onAnimationInfo")
}
this.animationControl = animationControl
@ -335,8 +331,8 @@ class ApngFrames private constructor(
this.canvas = Canvas(canvasBitmap)
}
override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) {
if(debug) {
override fun onDefaultImage(apng: Apng, bitmap: ApngBitmap) {
if (debug) {
Log.d(TAG, "onDefaultImage")
}
defaultImage?.recycle()
@ -344,11 +340,11 @@ class ApngFrames private constructor(
}
override fun onAnimationFrame(
apng : Apng,
frameControl : ApngFrameControl,
frameBitmap : ApngBitmap
apng: Apng,
frameControl: ApngFrameControl,
frameBitmap: ApngBitmap,
) {
if(debug) {
if (debug) {
Log.d(
TAG,
"onAnimationFrame seq=${frameControl.sequenceNumber}, xywh=${frameControl.xOffset},${frameControl.yOffset},${frameControl.width},${frameControl.height} blendOp=${frameControl.blendOp}, disposeOp=${frameControl.disposeOp},delay=${frameControl.delayMilliseconds}"
@ -365,7 +361,7 @@ class ApngFrames private constructor(
else -> frameControl.disposeOp
}
val previous : Bitmap? = when(disposeOp) {
val previous: Bitmap? = when (disposeOp) {
DisposeOp.Previous -> Bitmap.createBitmap(
canvasBitmap,
frameControl.xOffset,
@ -386,7 +382,7 @@ class ApngFrames private constructor(
frameBitmapAndroid,
frameControl.xOffset.toFloat(),
frameControl.yOffset.toFloat(),
when(frameControl.blendOp) {
when (frameControl.blendOp) {
// all color components of the frame, including alpha,
// overwrite the current contents of the frame's output buffer region.
BlendOp.Source -> sPaintDontBlend
@ -404,7 +400,7 @@ class ApngFrames private constructor(
frames.add(frame)
timeTotal += frame.timeWidth
when(disposeOp) {
when (disposeOp) {
// no disposal is done on this frame before rendering the next;
// the contents of the output buffer are left as is.
@ -427,7 +423,7 @@ class ApngFrames private constructor(
// the frame's region of the output buffer is
// to be reverted to the previous contents
// before rendering the next frame.
DisposeOp.Previous -> if(previous != null) {
DisposeOp.Previous -> if (previous != null) {
canvas.drawBitmap(
previous,
frameControl.xOffset.toFloat(),
@ -435,9 +431,7 @@ class ApngFrames private constructor(
sPaintDontBlend
)
}
}
} finally {
frameBitmapAndroid.recycle()
}
@ -449,25 +443,25 @@ class ApngFrames private constructor(
///////////////////////////////////////////////////////////////////////
// Gif support
override fun onGifWarning(message : String) {
override fun onGifWarning(message: String) {
Log.w(TAG, message)
}
override fun onGifDebug(message : String) {
override fun onGifDebug(message: String) {
Log.d(TAG, message)
}
override fun canGifDebug() : Boolean = debug
override fun canGifDebug(): Boolean = debug
override fun onGifHeader(header : ApngImageHeader) {
override fun onGifHeader(header: ApngImageHeader) {
this.header = header
}
override fun onGifAnimationInfo(
header : ApngImageHeader,
animationControl : ApngAnimationControl
header: ApngImageHeader,
animationControl: ApngAnimationControl,
) {
if(debug) {
if (debug) {
Log.d(TAG, "onAnimationInfo")
}
this.animationControl = animationControl
@ -478,10 +472,10 @@ class ApngFrames private constructor(
}
override fun onGifAnimationFrame(
frameControl : ApngFrameControl,
frameBitmap : ApngBitmap
frameControl: ApngFrameControl,
frameBitmap: ApngBitmap,
) {
if(debug) {
if (debug) {
Log.d(
TAG,
"onAnimationFrame seq=${frameControl.sequenceNumber}, xywh=${frameControl.xOffset},${frameControl.yOffset},${frameControl.width},${frameControl.height} blendOp=${frameControl.blendOp}, disposeOp=${frameControl.disposeOp},delay=${frameControl.delayMilliseconds}"
@ -489,7 +483,7 @@ class ApngFrames private constructor(
}
val frames = this.frames ?: return
if(frames.isEmpty()) {
if (frames.isEmpty()) {
defaultImage?.recycle()
defaultImage = toAndroidBitmap(frameBitmap, pixelSizeMax)
// ここでwidth,heightがセットされる
@ -504,11 +498,8 @@ class ApngFrames private constructor(
)
frames.add(frame)
timeTotal += frame.timeWidth
} finally {
frameBitmapAndroid.recycle()
}
}
}

View File

@ -135,6 +135,7 @@ dependencies {
implementation project(':apng_android')
implementation fileTree(include: ['*.aar'], dir: 'src/main/libs')
// App1 使
api "org.conscrypt:conscrypt-android:2.5.2"
@ -143,19 +144,48 @@ dependencies {
kapt "com.github.bumptech.glide:compiler:$glideVersion"
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 {
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) {
description = "Custom DETEKT build for all modules"
@ -173,12 +203,30 @@ tasks.register("detektAll", Detekt) {
// preconfigure defaults
buildUponDefaultConfig = true
setSource(projectSource)
def configFile = files("$rootDir/config/detekt/config.yml")
config.setFrom(configFile)
def baselineFile = file("$rootDir/config/detekt/baseline.xml")
if (baselineFile.isFile()) {
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)
reports {
html.enabled = true

View File

@ -1,7 +1,7 @@
package jp.juggler.subwaytooter
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.Test
import org.junit.runner.RunWith

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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.util.data.asciiPatternString
import org.junit.Assert.assertEquals

View File

@ -2,7 +2,7 @@ package jp.juggler.subwaytooter
import androidx.test.platform.app.InstrumentationRegistry
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.Test

View File

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

View File

@ -1,7 +1,7 @@
package jp.juggler.subwaytooter.api
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.table.SavedAccount
import jp.juggler.util.data.*

View File

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

View File

@ -1,7 +1,7 @@
package jp.juggler.subwaytooter.api.entity
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.table.SavedAccount
import jp.juggler.util.data.*

View File

@ -1,6 +1,6 @@
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 org.junit.Assert.assertEquals
import org.junit.Test

View File

@ -1,6 +1,6 @@
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.Test
import org.junit.runner.RunWith

View File

@ -4,7 +4,7 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
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.TABLE_LIST
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
import androidx.test.runner.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

View File

@ -1,9 +1,9 @@
package jp.juggler.subwaytooter.util
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.util.neatSpaces
import jp.juggler.util.data.*
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@ -17,7 +17,7 @@ class TestHtmlDecoder {
val start: Int,
val end: Int,
val flags: Int,
val text: String
val text: String,
) {
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.network.toFormRequestBody
import jp.juggler.util.network.toPost
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
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.attrColor
import jp.juggler.util.ui.createColoredDrawable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.anko.backgroundColor
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.util.LinkHelper
import jp.juggler.subwaytooter.util.VersionString
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
import jp.juggler.util.coroutine.launchDefault
import jp.juggler.util.data.*
import jp.juggler.util.log.LogCategory
@ -15,7 +16,6 @@ import jp.juggler.util.log.withCaption
import jp.juggler.util.network.toPostRequestBuilder
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
import okhttp3.Request
import java.util.regex.Pattern
import kotlin.coroutines.Continuation
@ -431,7 +431,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
try {
val req = requestQueue.receive()
val r = try {
withTimeout(30000L) {
withTimeoutSafe(30000L) {
handleRequest(req)
}
} catch (ex: Throwable) {
@ -481,7 +481,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
val cacheEntry = (hostArg ?: account?.apiHost ?: client.apiHost)?.getCacheEntry()
?: return tiError("missing host.")
return withTimeout(30000L) {
return withTimeoutSafe(30000L) {
suspendCoroutine { cont ->
QueuedRequest(cont, allowPixelfed) { cached ->

View File

@ -8,11 +8,11 @@ import android.net.wifi.WifiManager
import android.os.Build
import android.os.PowerManager
import jp.juggler.subwaytooter.App1
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
import jp.juggler.util.log.*
import jp.juggler.util.systemService
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
class CheckerWakeLocks(contextArg: Context) {
companion object {
@ -118,7 +118,7 @@ class CheckerWakeLocks(contextArg: Context) {
suspend fun checkConnection() {
var connectionState: String? = null
try {
withTimeout(10000L) {
withTimeoutSafe(10000L) {
while (true) {
connectionState = appState.networkTracker.connectionState
?: 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.toPutRequestBuilder
import jp.juggler.util.ui.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
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.ui.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import okhttp3.Request

View File

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

View File

@ -111,11 +111,18 @@ dependencies {
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"

View File

@ -1,16 +1,14 @@
package jp.juggler.base
import androidx.test.platform.app.InstrumentationRegistry
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.jugglerBaseNullable
import jp.juggler.base.JugglerBase.Companion.prepareJugglerBase
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
@ -23,6 +21,6 @@ class JugglerBaseTest {
assertNotNull("JubblerBase is initialized for a test.", jugglerBaseNullable)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
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
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.*
/**
* Test時にdispatcherを差し替えられるようにする
@ -33,4 +32,15 @@ object AppDispatchers {
default = 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 androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlin.coroutines.CoroutineContext

View File

@ -1,7 +1,5 @@
package jp.juggler.util.data
import java.util.LinkedHashMap
// same as x?.let{ dst.add(it) }
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
}

View File

@ -59,8 +59,8 @@ fun Cursor.getBlobOrNull(keyIdx: Int) =
fun Cursor.getBlobOrNull(key: String) =
getBlobOrNull(getColumnIndex(key))
fun Cursor.columnIndexOrThrow(key:String)=
getColumnIndex(key).takeIf{it>=0L} ?: error("missing column $key")
fun Cursor.columnIndexOrThrow(key: String) =
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.webkit.MimeTypeMap
import androidx.annotation.RawRes
import jp.juggler.util.data.getStringOrNull
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import okhttp3.internal.closeQuietly
import java.io.InputStream

View File

@ -69,7 +69,7 @@ class WordTrieTree {
// 単語の追加
fun add(
s: String,
tag:Any?=null,
tag: Any? = null,
validator: (src: CharSequence, start: Int, end: Int) -> Boolean = EMPTY_VALIDATOR,
) {
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
?: ArrayList<Any>().also{ node.matchTags = it}
?: ArrayList<Any>().also { node.matchTags = it }
tags.add(tag)
}
@ -136,7 +136,7 @@ class WordTrieTree {
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
@ -205,7 +205,7 @@ class WordTrieTree {
}
@OptIn(ExperimentalContracts::class)
fun WordTrieTree?.isNullOrEmpty() :Boolean {
fun WordTrieTree?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}

View File

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

View File

@ -37,7 +37,6 @@ internal class AlphaPatternDrawable(private val rectangleSize: Int) : Drawable()
*/
private var bitmap: Bitmap? = null
/**
* This will generate a bitmap with the pattern as big as the rectangle we were allow to draw on.
* We do this to chache the bitmap so we don't need to recreate it each time draw() is called since it takes a few milliseconds

View File

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

View File

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

View File

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

View File

@ -502,13 +502,13 @@ class ColorPickerView @JvmOverloads constructor(
return p
}
private fun satValToPoint(sat: Float, `val`: Float): Point {
val rect = satValRect
val height = rect!!.height().toFloat()
private fun satValToPoint(sat: Float, inValue: Float): Point {
val rect = satValRect!!
val height = rect.height().toFloat()
val width = rect.width().toFloat()
val p = Point()
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
}

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

View File

@ -125,7 +125,7 @@ class ActViewer : AsyncActivity() {
Log.d(TAG, "$title[$i] timeWidth=${f.timeWidth}")
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)
}

View File

@ -10,45 +10,48 @@ import android.view.View
import jp.juggler.apng.ApngFrames
import kotlin.math.max
class ApngView : View{
class ApngView : View {
var timeAnimationStart :Long =0L
var timeAnimationStart: Long = 0L
var apngFrames : ApngFrames? = null
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) {
constructor(context: Context) : super(context, null) {
init(context)
}
private var wView : Float = 1f
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, 0) {
init(context)
}
private var hView : Float = 1f
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 wImage: Float = 1f
private var hImage : Float = 1f
private var hImage: Float = 1f
private var aspectImage : Float = 1f
private var aspectImage: Float = 1f
private var currentScale: Float = 1f
private var currentScale : Float = 1f
private var currentTransX :Float = 0f
private var currentTransY :Float = 0f
private var currentTransX: Float = 0f
private var currentTransY: Float = 0f
private val drawMatrix = Matrix()
@ -56,13 +59,11 @@ class ApngView : View{
private val findFrameResult = ApngFrames.FindFrameResult()
private fun init(@Suppress("UNUSED_PARAMETER") context:Context){
private fun init(@Suppress("UNUSED_PARAMETER") context: Context) {
//
}
override fun onSizeChanged(w : Int, h : Int, oldw : Int, oldh : Int) {
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
wView = max(1, w).toFloat()
@ -72,14 +73,14 @@ class ApngView : View{
initializeScale()
}
private fun initializeScale(){
private fun initializeScale() {
val apngFrames = this.apngFrames
if( apngFrames != null) {
if (apngFrames != null) {
wImage = max(1, apngFrames.width).toFloat()
hImage = max(1, apngFrames.height).toFloat()
aspectImage = wImage / hImage
currentScale = if(aspectView > aspectImage) {
currentScale = if (aspectView > aspectImage) {
hView / hImage
} else {
wView / wImage
@ -90,7 +91,6 @@ class ApngView : View{
currentTransX = (wView - wDraw) / 2f
currentTransY = (hView - hDraw) / 2f
} else {
currentScale = 1f
currentTransX = 0f
@ -99,30 +99,28 @@ class ApngView : View{
invalidate()
}
override fun onDraw(canvas : Canvas) {
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val apngFrames = this.apngFrames
if(apngFrames != null ){
if (apngFrames != null) {
val t = SystemClock.elapsedRealtime() - timeAnimationStart
apngFrames.findFrame(findFrameResult,t)
apngFrames.findFrame(findFrameResult, t)
val delay = findFrameResult.delay
val bitmap = findFrameResult.bitmap
if( bitmap != null) {
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))
if (delay != Long.MAX_VALUE) {
postInvalidateDelayed(max(1L, delay))
}
}
}
}
}