runTestを使う。detektのチェック範囲を全モジュールとテストコードを含める。
This commit is contained in:
parent
b2b47e730a
commit
30b5beab64
|
@ -133,3 +133,4 @@ detektReport/
|
||||||
|
|
||||||
|
|
||||||
app/detekt-*.xml
|
app/detekt-*.xml
|
||||||
|
.idea/androidTestResultsUserPreferences.xml
|
||||||
|
|
|
@ -5,33 +5,32 @@ package jp.juggler.apng
|
||||||
import jp.juggler.apng.util.ByteSequence
|
import jp.juggler.apng.util.ByteSequence
|
||||||
|
|
||||||
class ApngAnimationControl(
|
class ApngAnimationControl(
|
||||||
|
|
||||||
// This must equal the number of `fcTL` chunks.
|
// This must equal the number of `fcTL` chunks.
|
||||||
// 0 is not a valid value.
|
// 0 is not a valid value.
|
||||||
// 1 is a valid value for a single-frame APNG.
|
// 1 is a valid value for a single-frame APNG.
|
||||||
val numFrames : Int,
|
val numFrames: Int,
|
||||||
|
|
||||||
// if it is 0, the animation should play indefinitely.
|
// if it is 0, the animation should play indefinitely.
|
||||||
// If nonzero, the animation should come to rest on the final frame at the end of the last play.
|
// If nonzero, the animation should come to rest on the final frame at the end of the last play.
|
||||||
val numPlays : Int = PLAY_INDEFINITELY
|
val numPlays: Int = PLAY_INDEFINITELY,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val PLAY_INDEFINITELY = 0
|
const val PLAY_INDEFINITELY = 0
|
||||||
|
|
||||||
internal fun parse(src : ByteSequence) : ApngAnimationControl {
|
internal fun parse(src: ByteSequence): ApngAnimationControl {
|
||||||
val numFrames = src.readInt32()
|
val numFrames = src.readInt32()
|
||||||
val numPlays = src.readInt32()
|
val numPlays = src.readInt32()
|
||||||
return ApngAnimationControl(
|
return ApngAnimationControl(
|
||||||
numFrames = numFrames,
|
numFrames = numFrames,
|
||||||
numPlays = numPlays
|
numPlays = numPlays
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
override fun toString() = "ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"
|
||||||
override fun toString() = "ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"
|
|
||||||
|
val isFinite: Boolean
|
||||||
val isFinite : Boolean
|
get() = numPlays > PLAY_INDEFINITELY
|
||||||
get() = numPlays > PLAY_INDEFINITELY
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,36 +4,36 @@ package jp.juggler.apng
|
||||||
|
|
||||||
import jp.juggler.apng.util.ByteSequence
|
import jp.juggler.apng.util.ByteSequence
|
||||||
|
|
||||||
class ApngBackground internal constructor(colorType : ColorType, src : ByteSequence) {
|
class ApngBackground internal constructor(colorType: ColorType, src: ByteSequence) {
|
||||||
|
|
||||||
val red : Int
|
val red: Int
|
||||||
val green : Int
|
val green: Int
|
||||||
val blue : Int
|
val blue: Int
|
||||||
val index : Int
|
val index: Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
when(colorType) {
|
when (colorType) {
|
||||||
ColorType.GREY, ColorType.GREY_ALPHA -> {
|
ColorType.GREY, ColorType.GREY_ALPHA -> {
|
||||||
val v = src.readUInt16()
|
val v = src.readUInt16()
|
||||||
red = v
|
red = v
|
||||||
green = v
|
green = v
|
||||||
blue = v
|
blue = v
|
||||||
index = - 1
|
index = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
ColorType.RGB, ColorType.RGBA -> {
|
ColorType.RGB, ColorType.RGBA -> {
|
||||||
red = src.readUInt16()
|
red = src.readUInt16()
|
||||||
green = src.readUInt16()
|
green = src.readUInt16()
|
||||||
blue = src.readUInt16()
|
blue = src.readUInt16()
|
||||||
index = - 1
|
index = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
ColorType.INDEX -> {
|
ColorType.INDEX -> {
|
||||||
red = - 1
|
red = -1
|
||||||
green = - 1
|
green = -1
|
||||||
blue = - 1
|
blue = -1
|
||||||
index = src.readUInt8()
|
index = src.readUInt8()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,63 +2,63 @@
|
||||||
|
|
||||||
package jp.juggler.apng
|
package jp.juggler.apng
|
||||||
|
|
||||||
class ApngBitmap(var width : Int, var height : Int) {
|
class ApngBitmap(var width: Int, var height: Int) {
|
||||||
|
|
||||||
// each int value contains 0xAARRGGBB
|
// each int value contains 0xAARRGGBB
|
||||||
val colors = IntArray(width * height)
|
val colors = IntArray(width * height)
|
||||||
|
|
||||||
// widthとheightを再指定する。ビットマップはそのまま再利用する
|
// widthとheightを再指定する。ビットマップはそのまま再利用する
|
||||||
fun reset(width : Int, height : Int) {
|
fun reset(width: Int, height: Int) {
|
||||||
val newSize = width * height
|
val newSize = width * height
|
||||||
if(newSize > colors.size)
|
if (newSize > colors.size)
|
||||||
throw ApngParseError("can't resize to $width x $height , it's greater than initial size")
|
throw ApngParseError("can't resize to $width x $height , it's greater than initial size")
|
||||||
this.width = width
|
this.width = width
|
||||||
this.height = height
|
this.height = height
|
||||||
// 透明な黒で初期化する
|
// 透明な黒で初期化する
|
||||||
colors.fill(0, fromIndex = 0, toIndex = newSize)
|
colors.fill(0, fromIndex = 0, toIndex = newSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ビットマップ中の位置を保持して、ピクセルへの書き込みと位置の更新を行う
|
// ビットマップ中の位置を保持して、ピクセルへの書き込みと位置の更新を行う
|
||||||
inner class Pointer {
|
inner class Pointer {
|
||||||
|
|
||||||
private var pos : Int = 0
|
private var pos: Int = 0
|
||||||
var step : Int = 1
|
var step: Int = 1
|
||||||
|
|
||||||
fun setPixel(argb : Int) = apply { colors[pos] = argb }
|
fun setPixel(argb: Int) = apply { colors[pos] = argb }
|
||||||
|
|
||||||
fun setPixel(a : Int, r : Int, g : Int, b : Int) = setPixel(
|
fun setPixel(a: Int, r: Int, g: Int, b: Int) = setPixel(
|
||||||
((a and 255) shl 24) or
|
((a and 255) shl 24) or
|
||||||
((r and 255) shl 16) or
|
((r and 255) shl 16) or
|
||||||
((g and 255) shl 8) or
|
((g and 255) shl 8) or
|
||||||
(b and 255)
|
(b and 255)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun setOffset(pos : Int = 0, step : Int = 1) = apply {
|
fun setOffset(pos: Int = 0, step: Int = 1) = apply {
|
||||||
this.pos = pos
|
this.pos = pos
|
||||||
this.step = step
|
this.step = step
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setXY(x : Int, y : Int, step : Int = 1) = setOffset(x + y * width, step)
|
fun setXY(x: Int, y: Int, step: Int = 1) = setOffset(x + y * width, step)
|
||||||
|
|
||||||
fun plus(x : Int) = apply { pos += x }
|
fun plus(x: Int) = apply { pos += x }
|
||||||
|
|
||||||
fun next() = plus(step)
|
fun next() = plus(step)
|
||||||
|
|
||||||
val color : Int
|
val color: Int
|
||||||
get() = colors[pos]
|
get() = colors[pos]
|
||||||
|
|
||||||
val alpha : Int
|
val alpha: Int
|
||||||
get() = (colors[pos] shr 24) and 255
|
get() = (colors[pos] shr 24) and 255
|
||||||
|
|
||||||
val red : Int
|
val red: Int
|
||||||
get() = (colors[pos] shr 16) and 255
|
get() = (colors[pos] shr 16) and 255
|
||||||
|
|
||||||
val green : Int
|
val green: Int
|
||||||
get() = (colors[pos] shr 8) and 255
|
get() = (colors[pos] shr 8) and 255
|
||||||
|
|
||||||
val blue : Int
|
val blue: Int
|
||||||
get() = (colors[pos]) and 255
|
get() = (colors[pos]) and 255
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pointer() = Pointer()
|
fun pointer() = Pointer()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,35 +5,34 @@ package jp.juggler.apng
|
||||||
import jp.juggler.apng.util.StreamTokenizer
|
import jp.juggler.apng.util.StreamTokenizer
|
||||||
import java.util.zip.CRC32
|
import java.util.zip.CRC32
|
||||||
|
|
||||||
internal class ApngChunk(crc32 : CRC32, tokenizer : StreamTokenizer) {
|
internal class ApngChunk(crc32: CRC32, tokenizer: StreamTokenizer) {
|
||||||
val size : Int
|
val size: Int
|
||||||
val type : String
|
val type: String
|
||||||
|
|
||||||
init {
|
init {
|
||||||
size = tokenizer.readInt32()
|
size = tokenizer.readInt32()
|
||||||
val typeBytes = tokenizer.readBytes(4)
|
val typeBytes = tokenizer.readBytes(4)
|
||||||
type = typeBytes.toString(Charsets.UTF_8)
|
type = typeBytes.toString(Charsets.UTF_8)
|
||||||
|
|
||||||
crc32.update(typeBytes)
|
crc32.update(typeBytes, 0, typeBytes.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readBody(crc32 : CRC32, tokenizer : StreamTokenizer) : ByteArray {
|
fun readBody(crc32: CRC32, tokenizer: StreamTokenizer): ByteArray {
|
||||||
val bytes = tokenizer.readBytes(size)
|
val bytes = tokenizer.readBytes(size)
|
||||||
val crcExpect = tokenizer.readUInt32()
|
val crcExpect = tokenizer.readUInt32()
|
||||||
|
|
||||||
crc32.update(bytes, 0, size)
|
crc32.update(bytes, 0, size)
|
||||||
val crcActual = crc32.value
|
val crcActual = crc32.value
|
||||||
if(crcActual != crcExpect) throw ApngParseError("CRC not match.")
|
if (crcActual != crcExpect) throw ApngParseError("CRC not match.")
|
||||||
|
|
||||||
return bytes
|
return bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
fun skipBody(tokenizer : StreamTokenizer) =
|
fun skipBody(tokenizer: StreamTokenizer) =
|
||||||
tokenizer.skipBytes((size + 4).toLong())
|
tokenizer.skipBytes((size + 4).toLong())
|
||||||
|
|
||||||
|
fun checkCRC(tokenizer: StreamTokenizer, crcActual: Long) {
|
||||||
fun checkCRC(tokenizer : StreamTokenizer, crcActual : Long) {
|
val crcExpect = tokenizer.readUInt32()
|
||||||
val crcExpect = tokenizer.readUInt32()
|
if (crcActual != crcExpect) throw ApngParseError("CRC not match.")
|
||||||
if(crcActual != crcExpect) throw ApngParseError("CRC not match.")
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,165 +7,165 @@ import java.io.InputStream
|
||||||
import java.util.zip.CRC32
|
import java.util.zip.CRC32
|
||||||
|
|
||||||
object ApngDecoder {
|
object ApngDecoder {
|
||||||
|
|
||||||
private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a)
|
private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a)
|
||||||
|
|
||||||
fun parseStream(
|
fun parseStream(
|
||||||
inStream : InputStream,
|
inStream: InputStream,
|
||||||
callback : ApngDecoderCallback
|
callback: ApngDecoderCallback,
|
||||||
) {
|
) {
|
||||||
val apng = Apng()
|
val apng = Apng()
|
||||||
val tokenizer = StreamTokenizer(inStream)
|
val tokenizer = StreamTokenizer(inStream)
|
||||||
|
|
||||||
val pngHeader = tokenizer.readBytes(8)
|
val pngHeader = tokenizer.readBytes(8)
|
||||||
if(! pngHeader.contentEquals(PNG_SIGNATURE)) {
|
if (!pngHeader.contentEquals(PNG_SIGNATURE)) {
|
||||||
throw ApngParseError("header not match")
|
throw ApngParseError("header not match")
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastSequenceNumber : Int? = null
|
var lastSequenceNumber: Int? = null
|
||||||
fun checkSequenceNumber(n : Int) {
|
fun checkSequenceNumber(n: Int) {
|
||||||
val last = lastSequenceNumber
|
val last = lastSequenceNumber
|
||||||
if(last != null && n <= last) {
|
if (last != null && n <= last) {
|
||||||
throw ApngParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n")
|
throw ApngParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n")
|
||||||
}
|
}
|
||||||
lastSequenceNumber = n
|
lastSequenceNumber = n
|
||||||
}
|
}
|
||||||
|
|
||||||
val inBuffer = ByteArray(4096)
|
val inBuffer = ByteArray(4096)
|
||||||
val inflateBufferPool = BufferPool(8192)
|
val inflateBufferPool = BufferPool(8192)
|
||||||
var idatDecoder : IdatDecoder? = null
|
var idatDecoder: IdatDecoder? = null
|
||||||
var fdatDecoder : IdatDecoder? = null
|
var fdatDecoder: IdatDecoder? = null
|
||||||
val crc32 = CRC32()
|
val crc32 = CRC32()
|
||||||
var lastFctl : ApngFrameControl? = null
|
var lastFctl: ApngFrameControl? = null
|
||||||
var bitmap : ApngBitmap? = null
|
var bitmap: ApngBitmap? = null
|
||||||
|
|
||||||
loop@ while(true) {
|
loop@ while (true) {
|
||||||
crc32.reset()
|
crc32.reset()
|
||||||
val chunk = ApngChunk(crc32, tokenizer)
|
val chunk = ApngChunk(crc32, tokenizer)
|
||||||
when(chunk.type) {
|
when (chunk.type) {
|
||||||
|
|
||||||
"IEND" -> break@loop
|
"IEND" -> break@loop
|
||||||
|
|
||||||
"IHDR" -> {
|
"IHDR" -> {
|
||||||
val header =
|
val header =
|
||||||
ApngImageHeader.parse(ByteSequence(chunk.readBody(crc32, tokenizer)))
|
ApngImageHeader.parse(ByteSequence(chunk.readBody(crc32, tokenizer)))
|
||||||
bitmap = ApngBitmap(header.width, header.height)
|
bitmap = ApngBitmap(header.width, header.height)
|
||||||
apng.header = header
|
apng.header = header
|
||||||
callback.onHeader(apng, header)
|
callback.onHeader(apng, header)
|
||||||
}
|
}
|
||||||
|
|
||||||
"PLTE" -> apng.palette = ApngPalette(chunk.readBody(crc32, tokenizer))
|
"PLTE" -> apng.palette = ApngPalette(chunk.readBody(crc32, tokenizer))
|
||||||
|
|
||||||
"bKGD" -> {
|
"bKGD" -> {
|
||||||
val header = apng.header ?: throw ApngParseError("missing IHDR")
|
val header = apng.header ?: throw ApngParseError("missing IHDR")
|
||||||
apng.background = ApngBackground(
|
apng.background = ApngBackground(
|
||||||
header.colorType,
|
header.colorType,
|
||||||
ByteSequence(chunk.readBody(crc32, tokenizer))
|
ByteSequence(chunk.readBody(crc32, tokenizer))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"tRNS" -> {
|
"tRNS" -> {
|
||||||
val header = apng.header ?: throw ApngParseError("missing IHDR")
|
val header = apng.header ?: throw ApngParseError("missing IHDR")
|
||||||
val body = chunk.readBody(crc32, tokenizer)
|
val body = chunk.readBody(crc32, tokenizer)
|
||||||
when(header.colorType) {
|
when (header.colorType) {
|
||||||
ColorType.GREY -> apng.transparentColor =
|
ColorType.GREY -> apng.transparentColor =
|
||||||
ApngTransparentColor(true, ByteSequence(body))
|
ApngTransparentColor(true, ByteSequence(body))
|
||||||
ColorType.RGB -> apng.transparentColor =
|
ColorType.RGB -> apng.transparentColor =
|
||||||
ApngTransparentColor(false, ByteSequence(body))
|
ApngTransparentColor(false, ByteSequence(body))
|
||||||
ColorType.INDEX -> apng.palette?.parseTRNS(body)
|
ColorType.INDEX -> apng.palette?.parseTRNS(body)
|
||||||
?: throw ApngParseError("missing palette")
|
?: throw ApngParseError("missing palette")
|
||||||
else -> callback.onApngWarning("tRNS ignored. colorType =${header.colorType}")
|
else -> callback.onApngWarning("tRNS ignored. colorType =${header.colorType}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"IDAT" -> {
|
"IDAT" -> {
|
||||||
val header = apng.header ?: throw ApngParseError("missing IHDR")
|
val header = apng.header ?: throw ApngParseError("missing IHDR")
|
||||||
if(idatDecoder == null) {
|
if (idatDecoder == null) {
|
||||||
bitmap ?: throw ApngParseError("missing bitmap")
|
bitmap ?: throw ApngParseError("missing bitmap")
|
||||||
bitmap.reset(header.width, header.height)
|
bitmap.reset(header.width, header.height)
|
||||||
idatDecoder = IdatDecoder(
|
idatDecoder = IdatDecoder(
|
||||||
apng,
|
apng,
|
||||||
bitmap,
|
bitmap,
|
||||||
inflateBufferPool,
|
inflateBufferPool,
|
||||||
callback
|
callback
|
||||||
) {
|
) {
|
||||||
callback.onDefaultImage(apng, bitmap)
|
callback.onDefaultImage(apng, bitmap)
|
||||||
val fctl = lastFctl
|
val fctl = lastFctl
|
||||||
if(fctl != null) {
|
if (fctl != null) {
|
||||||
// IDATより前にfcTLが登場しているなら、そのfcTLの画像はIDATと同じ
|
// IDATより前にfcTLが登場しているなら、そのfcTLの画像はIDATと同じ
|
||||||
callback.onAnimationFrame(apng, fctl, bitmap)
|
callback.onAnimationFrame(apng, fctl, bitmap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
idatDecoder.addData(
|
idatDecoder.addData(
|
||||||
tokenizer.inStream,
|
tokenizer.inStream,
|
||||||
chunk.size,
|
chunk.size,
|
||||||
inBuffer,
|
inBuffer,
|
||||||
crc32
|
crc32
|
||||||
)
|
)
|
||||||
chunk.checkCRC(tokenizer, crc32.value)
|
chunk.checkCRC(tokenizer, crc32.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
"acTL" -> {
|
"acTL" -> {
|
||||||
val header = apng.header ?: throw ApngParseError("missing IHDR")
|
val header = apng.header ?: throw ApngParseError("missing IHDR")
|
||||||
val animationControl = ApngAnimationControl
|
val animationControl = ApngAnimationControl
|
||||||
.parse(ByteSequence(chunk.readBody(crc32, tokenizer)))
|
.parse(ByteSequence(chunk.readBody(crc32, tokenizer)))
|
||||||
apng.animationControl = animationControl
|
apng.animationControl = animationControl
|
||||||
callback.onAnimationInfo(apng, header, animationControl)
|
callback.onAnimationInfo(apng, header, animationControl)
|
||||||
}
|
}
|
||||||
|
|
||||||
"fcTL" -> {
|
"fcTL" -> {
|
||||||
val bat = ByteSequence(chunk.readBody(crc32, tokenizer))
|
val bat = ByteSequence(chunk.readBody(crc32, tokenizer))
|
||||||
val sequenceNumber = bat.readInt32()
|
val sequenceNumber = bat.readInt32()
|
||||||
checkSequenceNumber(sequenceNumber)
|
checkSequenceNumber(sequenceNumber)
|
||||||
lastFctl = ApngFrameControl.parse(bat, sequenceNumber)
|
lastFctl = ApngFrameControl.parse(bat, sequenceNumber)
|
||||||
fdatDecoder = null
|
fdatDecoder = null
|
||||||
}
|
}
|
||||||
|
|
||||||
"fdAT" -> {
|
"fdAT" -> {
|
||||||
val fctl = lastFctl ?: throw ApngParseError("missing fCTL before fdAT")
|
val fctl = lastFctl ?: throw ApngParseError("missing fCTL before fdAT")
|
||||||
if(fdatDecoder == null) {
|
if (fdatDecoder == null) {
|
||||||
bitmap ?: throw ApngParseError("missing bitmap")
|
bitmap ?: throw ApngParseError("missing bitmap")
|
||||||
bitmap.reset(fctl.width, fctl.height)
|
bitmap.reset(fctl.width, fctl.height)
|
||||||
fdatDecoder = IdatDecoder(
|
fdatDecoder = IdatDecoder(
|
||||||
apng,
|
apng,
|
||||||
bitmap,
|
bitmap,
|
||||||
inflateBufferPool,
|
inflateBufferPool,
|
||||||
callback
|
callback
|
||||||
) {
|
) {
|
||||||
callback.onAnimationFrame(apng, fctl, bitmap)
|
callback.onAnimationFrame(apng, fctl, bitmap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val sequenceNumber = tokenizer.readInt32(crc32)
|
val sequenceNumber = tokenizer.readInt32(crc32)
|
||||||
checkSequenceNumber(sequenceNumber)
|
checkSequenceNumber(sequenceNumber)
|
||||||
fdatDecoder.addData(
|
fdatDecoder.addData(
|
||||||
tokenizer.inStream,
|
tokenizer.inStream,
|
||||||
chunk.size - 4,
|
chunk.size - 4,
|
||||||
inBuffer,
|
inBuffer,
|
||||||
crc32
|
crc32
|
||||||
)
|
)
|
||||||
chunk.checkCRC(tokenizer, crc32.value)
|
chunk.checkCRC(tokenizer, crc32.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 無視するチャンク
|
// 無視するチャンク
|
||||||
"cHRM", "gAMA", "iCCP", "sBIT", "sRGB", // color space information
|
"cHRM", "gAMA", "iCCP", "sBIT", "sRGB", // color space information
|
||||||
"tEXt", "zTXt", "iTXt", // text information
|
"tEXt", "zTXt", "iTXt", // text information
|
||||||
"tIME", // timestamp
|
"tIME", // timestamp
|
||||||
"hIST", // histogram
|
"hIST", // histogram
|
||||||
"pHYs", // Physical pixel dimensions
|
"pHYs", // Physical pixel dimensions
|
||||||
"sPLT" // Suggested palette (おそらく減色用?)
|
"sPLT", // Suggested palette (おそらく減色用?)
|
||||||
-> chunk.skipBody(tokenizer)
|
-> chunk.skipBody(tokenizer)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
callback.onApngWarning(
|
callback.onApngWarning(
|
||||||
"unknown chunk: type=%s,size=0x%x".format(
|
"unknown chunk: type=%s,size=0x%x".format(
|
||||||
chunk.type,
|
chunk.type,
|
||||||
chunk.size
|
chunk.size
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
chunk.skipBody(tokenizer)
|
chunk.skipBody(tokenizer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,30 @@
|
||||||
package jp.juggler.apng
|
package jp.juggler.apng
|
||||||
|
|
||||||
interface ApngDecoderCallback {
|
interface ApngDecoderCallback {
|
||||||
|
|
||||||
// called for non-fatal warning
|
// called for non-fatal warning
|
||||||
fun onApngWarning(message : String)
|
fun onApngWarning(message: String)
|
||||||
|
|
||||||
// called for debug message
|
// called for debug message
|
||||||
fun onApngDebug(message : String) {}
|
fun onApngDebug(message: String) {}
|
||||||
|
|
||||||
fun canApngDebug() : Boolean = false
|
fun canApngDebug(): Boolean = false
|
||||||
|
|
||||||
// called when PNG image header is detected.
|
// called when PNG image header is detected.
|
||||||
fun onHeader(apng : Apng, header : ApngImageHeader)
|
fun onHeader(apng: Apng, header: ApngImageHeader)
|
||||||
|
|
||||||
// called when APNG Animation Control is detected.
|
// called when APNG Animation Control is detected.
|
||||||
fun onAnimationInfo(
|
fun onAnimationInfo(
|
||||||
apng : Apng,
|
apng: Apng,
|
||||||
header : ApngImageHeader,
|
header: ApngImageHeader,
|
||||||
animationControl : ApngAnimationControl
|
animationControl: ApngAnimationControl,
|
||||||
)
|
)
|
||||||
|
|
||||||
// called when default image bitmap was rendered.
|
// called when default image bitmap was rendered.
|
||||||
fun onDefaultImage(apng : Apng, bitmap : ApngBitmap)
|
fun onDefaultImage(apng: Apng, bitmap: ApngBitmap)
|
||||||
|
|
||||||
// called when APNG Frame Control is detected and its bitmap was rendered.
|
// called when APNG Frame Control is detected and its bitmap was rendered.
|
||||||
// its bitmap may same to default image for first frame.
|
// its bitmap may same to default image for first frame.
|
||||||
// ( in this case, both of onDefaultImage and onAnimationFrame are called for same bitmap)
|
// ( in this case, both of onDefaultImage and onAnimationFrame are called for same bitmap)
|
||||||
fun onAnimationFrame(apng : Apng, frameControl : ApngFrameControl, frameBitmap : ApngBitmap)
|
fun onAnimationFrame(apng: Apng, frameControl: ApngFrameControl, frameBitmap: ApngBitmap)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,51 +4,50 @@ package jp.juggler.apng
|
||||||
|
|
||||||
import jp.juggler.apng.util.ByteSequence
|
import jp.juggler.apng.util.ByteSequence
|
||||||
|
|
||||||
class ApngFrameControl (
|
class ApngFrameControl(
|
||||||
val width : Int,
|
val width: Int,
|
||||||
val height : Int,
|
val height: Int,
|
||||||
val xOffset : Int,
|
val xOffset: Int,
|
||||||
val yOffset : Int,
|
val yOffset: Int,
|
||||||
val disposeOp : DisposeOp,
|
val disposeOp: DisposeOp,
|
||||||
val blendOp : BlendOp,
|
val blendOp: BlendOp,
|
||||||
val sequenceNumber:Int,
|
val sequenceNumber: Int,
|
||||||
val delayMilliseconds: Long
|
val delayMilliseconds: Long,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object{
|
|
||||||
internal fun parse(src : ByteSequence, sequenceNumber:Int) :ApngFrameControl{
|
|
||||||
val width = src.readInt32()
|
|
||||||
val height = src.readInt32()
|
|
||||||
val xOffset = src.readInt32()
|
|
||||||
val yOffset = src.readInt32()
|
|
||||||
val delayNum = src.readUInt16()
|
|
||||||
val delayDen = src.readUInt16().let { if(it == 0) 100 else it }
|
|
||||||
|
|
||||||
var num : Int
|
|
||||||
|
|
||||||
num = src.readUInt8()
|
|
||||||
val disposeOp = DisposeOp.values().first { it.num == num }
|
|
||||||
|
|
||||||
num = src.readUInt8()
|
|
||||||
val blendOp = BlendOp.values().first { it.num == num }
|
|
||||||
|
|
||||||
return ApngFrameControl(
|
companion object {
|
||||||
width =width,
|
internal fun parse(src: ByteSequence, sequenceNumber: Int): ApngFrameControl {
|
||||||
height = height,
|
val width = src.readInt32()
|
||||||
xOffset = xOffset,
|
val height = src.readInt32()
|
||||||
yOffset = yOffset,
|
val xOffset = src.readInt32()
|
||||||
disposeOp = disposeOp,
|
val yOffset = src.readInt32()
|
||||||
blendOp = blendOp,
|
val delayNum = src.readUInt16()
|
||||||
sequenceNumber = sequenceNumber,
|
val delayDen = src.readUInt16().let { if (it == 0) 100 else it }
|
||||||
delayMilliseconds = when(delayDen) {
|
|
||||||
0,1000 -> delayNum.toLong()
|
|
||||||
else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString() =
|
|
||||||
"ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayMilliseconds=$delayMilliseconds,disposeOp=$disposeOp,blendOp=$blendOp)"
|
|
||||||
|
|
||||||
|
var num: Int
|
||||||
|
|
||||||
|
num = src.readUInt8()
|
||||||
|
val disposeOp = DisposeOp.values().first { it.num == num }
|
||||||
|
|
||||||
|
num = src.readUInt8()
|
||||||
|
val blendOp = BlendOp.values().first { it.num == num }
|
||||||
|
|
||||||
|
return ApngFrameControl(
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
xOffset = xOffset,
|
||||||
|
yOffset = yOffset,
|
||||||
|
disposeOp = disposeOp,
|
||||||
|
blendOp = blendOp,
|
||||||
|
sequenceNumber = sequenceNumber,
|
||||||
|
delayMilliseconds = when (delayDen) {
|
||||||
|
0, 1000 -> delayNum.toLong()
|
||||||
|
else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString() =
|
||||||
|
"ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayMilliseconds=$delayMilliseconds,disposeOp=$disposeOp,blendOp=$blendOp)"
|
||||||
}
|
}
|
|
@ -6,50 +6,48 @@ import jp.juggler.apng.util.ByteSequence
|
||||||
|
|
||||||
// information from IHDR chunk.
|
// information from IHDR chunk.
|
||||||
class ApngImageHeader(
|
class ApngImageHeader(
|
||||||
val width : Int,
|
val width: Int,
|
||||||
val height : Int,
|
val height: Int,
|
||||||
val bitDepth : Int,
|
val bitDepth: Int,
|
||||||
val colorType : ColorType,
|
val colorType: ColorType,
|
||||||
val compressionMethod : CompressionMethod,
|
val compressionMethod: CompressionMethod,
|
||||||
val filterMethod : FilterMethod,
|
val filterMethod: FilterMethod,
|
||||||
val interlaceMethod : InterlaceMethod
|
val interlaceMethod: InterlaceMethod,
|
||||||
) {
|
) {
|
||||||
companion object{
|
companion object {
|
||||||
internal fun parse (src : ByteSequence) :ApngImageHeader{
|
internal fun parse(src: ByteSequence): ApngImageHeader {
|
||||||
|
val width = src.readInt32()
|
||||||
val width = src.readInt32()
|
val height = src.readInt32()
|
||||||
val height = src.readInt32()
|
if (width <= 0 || height <= 0) throw ApngParseError("w=$width,h=$height is too small")
|
||||||
if(width <= 0 || height <= 0) throw ApngParseError("w=$width,h=$height is too small")
|
|
||||||
|
|
||||||
val bitDepth = src.readUInt8()
|
|
||||||
|
|
||||||
var num : Int
|
|
||||||
//
|
|
||||||
num = src.readUInt8()
|
|
||||||
val colorType = ColorType.values().first { it.num == num }
|
|
||||||
//
|
|
||||||
num = src.readUInt8()
|
|
||||||
val compressionMethod = CompressionMethod.values().first { it.num == num }
|
|
||||||
//
|
|
||||||
num = src.readUInt8()
|
|
||||||
val filterMethod = FilterMethod.values().first { it.num == num }
|
|
||||||
//
|
|
||||||
num = src.readUInt8()
|
|
||||||
val interlaceMethod = InterlaceMethod.values().first { it.num == num }
|
|
||||||
|
|
||||||
return ApngImageHeader(
|
val bitDepth = src.readUInt8()
|
||||||
width =width,
|
|
||||||
height = height,
|
var num: Int
|
||||||
bitDepth = bitDepth,
|
//
|
||||||
colorType = colorType,
|
num = src.readUInt8()
|
||||||
compressionMethod = compressionMethod,
|
val colorType = ColorType.values().first { it.num == num }
|
||||||
filterMethod = filterMethod,
|
//
|
||||||
interlaceMethod = interlaceMethod
|
num = src.readUInt8()
|
||||||
|
val compressionMethod = CompressionMethod.values().first { it.num == num }
|
||||||
)
|
//
|
||||||
}
|
num = src.readUInt8()
|
||||||
}
|
val filterMethod = FilterMethod.values().first { it.num == num }
|
||||||
|
//
|
||||||
override fun toString() =
|
num = src.readUInt8()
|
||||||
"ApngImageHeader(w=$width,h=$height,bits=$bitDepth,color=$colorType,interlace=$interlaceMethod)"
|
val interlaceMethod = InterlaceMethod.values().first { it.num == num }
|
||||||
|
|
||||||
|
return ApngImageHeader(
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
bitDepth = bitDepth,
|
||||||
|
colorType = colorType,
|
||||||
|
compressionMethod = compressionMethod,
|
||||||
|
filterMethod = filterMethod,
|
||||||
|
interlaceMethod = interlaceMethod,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString() =
|
||||||
|
"ApngImageHeader(w=$width,h=$height,bits=$bitDepth,color=$colorType,interlace=$interlaceMethod)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,38 +6,38 @@ import jp.juggler.apng.util.getUInt8
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class ApngPalette(
|
class ApngPalette(
|
||||||
src : ByteArray // repeat of R,G,B
|
src: ByteArray, // repeat of R,G,B
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// full opaque black
|
// full opaque black
|
||||||
const val OPAQUE = 255 shl 24
|
const val OPAQUE = 255 shl 24
|
||||||
}
|
}
|
||||||
|
|
||||||
val list : IntArray // repeat of 0xAARRGGBB
|
val list: IntArray // repeat of 0xAARRGGBB
|
||||||
|
|
||||||
var hasAlpha : Boolean = false
|
var hasAlpha: Boolean = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val entryCount = src.size / 3
|
val entryCount = src.size / 3
|
||||||
list = IntArray(entryCount)
|
list = IntArray(entryCount)
|
||||||
var pos = 0
|
var pos = 0
|
||||||
for(i in 0 until entryCount) {
|
for (i in 0 until entryCount) {
|
||||||
list[i] = OPAQUE or
|
list[i] = OPAQUE or
|
||||||
(src.getUInt8(pos) shl 16) or
|
(src.getUInt8(pos) shl 16) or
|
||||||
(src.getUInt8(pos + 1) shl 8) or
|
(src.getUInt8(pos + 1) shl 8) or
|
||||||
src.getUInt8(pos + 2)
|
src.getUInt8(pos + 2)
|
||||||
pos += 3
|
pos += 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString() = "palette(${list.size} entries,hasAlpha=$hasAlpha)"
|
override fun toString() = "palette(${list.size} entries,hasAlpha=$hasAlpha)"
|
||||||
|
|
||||||
// update alpha value from tRNS chunk data
|
// update alpha value from tRNS chunk data
|
||||||
fun parseTRNS(ba : ByteArray) {
|
fun parseTRNS(ba: ByteArray) {
|
||||||
hasAlpha = true
|
hasAlpha = true
|
||||||
for(i in 0 until min(list.size, ba.size)) {
|
for (i in 0 until min(list.size, ba.size)) {
|
||||||
list[i] = (list[i] and 0xffffff) or (ba.getUInt8(i) shl 24)
|
list[i] = (list[i] and 0xffffff) or (ba.getUInt8(i) shl 24)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,24 +4,24 @@ package jp.juggler.apng
|
||||||
|
|
||||||
import jp.juggler.apng.util.ByteSequence
|
import jp.juggler.apng.util.ByteSequence
|
||||||
|
|
||||||
class ApngTransparentColor internal constructor(isGreyScale : Boolean, src : ByteSequence) {
|
class ApngTransparentColor internal constructor(isGreyScale: Boolean, src: ByteSequence) {
|
||||||
val red : Int
|
val red: Int
|
||||||
val green : Int
|
val green: Int
|
||||||
val blue : Int
|
val blue: Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if(isGreyScale) {
|
if (isGreyScale) {
|
||||||
val v = src.readUInt16()
|
val v = src.readUInt16()
|
||||||
red = v
|
red = v
|
||||||
green = v
|
green = v
|
||||||
blue = v
|
blue = v
|
||||||
} else {
|
} else {
|
||||||
red = src.readUInt16()
|
red = src.readUInt16()
|
||||||
green = src.readUInt16()
|
green = src.readUInt16()
|
||||||
blue = src.readUInt16()
|
blue = src.readUInt16()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun match(grey : Int) = red == grey
|
fun match(grey: Int) = red == grey
|
||||||
fun match(r : Int, g : Int, b : Int) = (r == red && g == green && b == blue)
|
fun match(r: Int, g: Int, b: Int) = (r == red && g == green && b == blue)
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,11 +1,10 @@
|
||||||
package jp.juggler.apng
|
package jp.juggler.apng
|
||||||
|
|
||||||
interface GifDecoderCallback {
|
interface GifDecoderCallback {
|
||||||
fun onGifWarning(message : String)
|
fun onGifWarning(message: String)
|
||||||
fun onGifDebug(message : String)
|
fun onGifDebug(message: String)
|
||||||
fun canGifDebug() : Boolean
|
fun canGifDebug(): Boolean
|
||||||
fun onGifHeader(header : ApngImageHeader)
|
fun onGifHeader(header: ApngImageHeader)
|
||||||
fun onGifAnimationInfo( header : ApngImageHeader, animationControl : ApngAnimationControl )
|
fun onGifAnimationInfo(header: ApngImageHeader, animationControl: ApngAnimationControl)
|
||||||
fun onGifAnimationFrame( frameControl : ApngFrameControl, frameBitmap : ApngBitmap )
|
fun onGifAnimationFrame(frameControl: ApngFrameControl, frameBitmap: ApngBitmap)
|
||||||
|
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@ internal class IdatDecoder(
|
||||||
private val bitmap: ApngBitmap,
|
private val bitmap: ApngBitmap,
|
||||||
private val inflateBufferPool: BufferPool,
|
private val inflateBufferPool: BufferPool,
|
||||||
private val callback: ApngDecoderCallback,
|
private val callback: ApngDecoderCallback,
|
||||||
private val onCompleted: () -> Unit
|
private val onCompleted: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private class PassInfo(val xStep: Int, val xStart: Int, val yStep: Int, val yStart: Int)
|
private class PassInfo(val xStep: Int, val xStart: Int, val yStep: Int, val yStart: Int)
|
||||||
|
@ -47,9 +47,9 @@ internal class IdatDecoder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun scanLine1(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
private inline fun scanLine1(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) {
|
||||||
var pos = 1
|
var pos = 1
|
||||||
var remain = pass_w
|
var remain = passW
|
||||||
while (remain >= 8) {
|
while (remain >= 8) {
|
||||||
remain -= 8
|
remain -= 8
|
||||||
val v = baLine[pos++].toInt()
|
val v = baLine[pos++].toInt()
|
||||||
|
@ -74,9 +74,9 @@ internal class IdatDecoder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun scanLine2(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
private inline fun scanLine2(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) {
|
||||||
var pos = 1
|
var pos = 1
|
||||||
var remain = pass_w
|
var remain = passW
|
||||||
while (remain >= 4) {
|
while (remain >= 4) {
|
||||||
remain -= 4
|
remain -= 4
|
||||||
val v = baLine[pos++].toInt()
|
val v = baLine[pos++].toInt()
|
||||||
|
@ -93,9 +93,9 @@ internal class IdatDecoder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun scanLine4(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
private inline fun scanLine4(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) {
|
||||||
var pos = 1
|
var pos = 1
|
||||||
var remain = pass_w
|
var remain = passW
|
||||||
while (remain >= 2) {
|
while (remain >= 2) {
|
||||||
remain -= 2
|
remain -= 2
|
||||||
val v = baLine[pos++].toInt()
|
val v = baLine[pos++].toInt()
|
||||||
|
@ -108,14 +108,13 @@ internal class IdatDecoder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun scanLine8(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
private inline fun scanLine8(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) {
|
||||||
var pos = 1
|
var pos = 1
|
||||||
var remain = pass_w
|
var remain = passW
|
||||||
while (remain-- > 0) {
|
while (remain-- > 0) {
|
||||||
block(baLine.getUInt8(pos++))
|
block(baLine.getUInt8(pos++))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val inflater = Inflater()
|
private val inflater = Inflater()
|
||||||
|
@ -405,7 +404,7 @@ internal class IdatDecoder(
|
||||||
|
|
||||||
val filterNum = baLine.getUInt8(0)
|
val filterNum = baLine.getUInt8(0)
|
||||||
|
|
||||||
// if( callback.canApngDebug() ) callback.onApngDebug("y=$passY/${passHeight},filterType=$filterType")
|
// if( callback.canApngDebug() ) callback.onApngDebug("y=$passY/${passHeight},filterType=$filterType")
|
||||||
|
|
||||||
when (FilterType.values().first { it.num == filterNum }) {
|
when (FilterType.values().first { it.num == filterNum }) {
|
||||||
FilterType.None -> {
|
FilterType.None -> {
|
||||||
|
@ -424,7 +423,6 @@ internal class IdatDecoder(
|
||||||
// val y = passInfo.yStart + passInfo.yStep * passY
|
// val y = passInfo.yStart + passInfo.yStep * passY
|
||||||
// callback.onApngDebug("sub pos=$pos,x=$x,y=$y,left=$vLeft,cur=$vCur,after=${baLine[pos].toInt() and 255}")
|
// callback.onApngDebug("sub pos=$pos,x=$x,y=$y,left=$vLeft,cur=$vCur,after=${baLine[pos].toInt() and 255}")
|
||||||
// }
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -464,7 +462,6 @@ internal class IdatDecoder(
|
||||||
// val y = passInfo.yStart + passInfo.yStep * passY
|
// val y = passInfo.yStart + passInfo.yStep * passY
|
||||||
// callback.onApngDebug("paeth pos=$pos,x=$x,y=$y,left=$vLeft,up=$vUp,ul=$vUpperLeft,cur=$vCur,paeth=${paeth(vLeft, vUp, vUpperLeft)}")
|
// callback.onApngDebug("paeth pos=$pos,x=$x,y=$y,left=$vLeft,up=$vUp,ul=$vUpperLeft,cur=$vCur,paeth=${paeth(vLeft, vUp, vUpperLeft)}")
|
||||||
// }
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -498,7 +495,7 @@ internal class IdatDecoder(
|
||||||
inStream: InputStream,
|
inStream: InputStream,
|
||||||
size: Int,
|
size: Int,
|
||||||
inBuffer: ByteArray,
|
inBuffer: ByteArray,
|
||||||
crc32: CRC32
|
crc32: CRC32,
|
||||||
) {
|
) {
|
||||||
var foundEnd = false
|
var foundEnd = false
|
||||||
var inRemain = size
|
var inRemain = size
|
||||||
|
@ -541,11 +538,11 @@ internal class IdatDecoder(
|
||||||
inflateBufferQueue.add(ByteSequence(buffer, 0, nInflated))
|
inflateBufferQueue.add(ByteSequence(buffer, 0, nInflated))
|
||||||
|
|
||||||
// キューに追加したデータをScanLine単位で消費する
|
// キューに追加したデータをScanLine単位で消費する
|
||||||
@Suppress("ControlFlowWithEmptyBody")
|
@Suppress("ControlFlowWithEmptyBody", "EmptyWhileBlock")
|
||||||
while (!isCompleted && readScanLine()){
|
while (!isCompleted && readScanLine()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCompleted) {
|
if (isCompleted) {
|
||||||
inflateBufferQueue.clear()
|
inflateBufferQueue.clear()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@ package jp.juggler.apng.util
|
||||||
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
internal class BufferPool(private val arraySize : Int) {
|
internal class BufferPool(private val arraySize: Int) {
|
||||||
private val list = LinkedList<ByteArray>()
|
private val list = LinkedList<ByteArray>()
|
||||||
fun obtain() : ByteArray = if(list.isEmpty()) ByteArray(arraySize) else list.removeFirst()
|
fun obtain(): ByteArray = if (list.isEmpty()) ByteArray(arraySize) else list.removeFirst()
|
||||||
fun recycle(array : ByteArray?) = array?.let { list.add(it) }
|
fun recycle(array: ByteArray?) = array?.let { list.add(it) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,32 +2,32 @@ package jp.juggler.apng.util
|
||||||
|
|
||||||
import jp.juggler.apng.ApngParseError
|
import jp.juggler.apng.ApngParseError
|
||||||
|
|
||||||
internal fun ByteArray.getUInt8(pos : Int) = get(pos).toInt() and 255
|
internal fun ByteArray.getUInt8(pos: Int) = get(pos).toInt() and 255
|
||||||
|
|
||||||
internal fun ByteArray.getUInt16(pos : Int) = (getUInt8(pos) shl 8) or getUInt8(pos + 1)
|
internal fun ByteArray.getUInt16(pos: Int) = (getUInt8(pos) shl 8) or getUInt8(pos + 1)
|
||||||
|
|
||||||
internal fun ByteArray.getInt32(pos : Int) = (getUInt8(pos) shl 24) or
|
internal fun ByteArray.getInt32(pos: Int) = (getUInt8(pos) shl 24) or
|
||||||
(getUInt8(pos + 1) shl 16) or
|
(getUInt8(pos + 1) shl 16) or
|
||||||
(getUInt8(pos + 2) shl 8) or
|
(getUInt8(pos + 2) shl 8) or
|
||||||
getUInt8(pos + 3)
|
getUInt8(pos + 3)
|
||||||
|
|
||||||
internal class ByteSequence(
|
internal class ByteSequence(
|
||||||
val array : ByteArray,
|
val array: ByteArray,
|
||||||
var offset : Int,
|
var offset: Int,
|
||||||
var length : Int
|
var length: Int,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
constructor(ba : ByteArray) : this(ba, 0, ba.size)
|
constructor(ba: ByteArray) : this(ba, 0, ba.size)
|
||||||
|
|
||||||
private inline fun <T> readX(dataSize : Int, block : () -> T) : T {
|
private inline fun <T> readX(dataSize: Int, block: () -> T): T {
|
||||||
if(length < dataSize) throw ApngParseError("readX: unexpected end")
|
if (length < dataSize) throw ApngParseError("readX: unexpected end")
|
||||||
val v = block()
|
val v = block()
|
||||||
offset += dataSize
|
offset += dataSize
|
||||||
length -= dataSize
|
length -= dataSize
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readUInt8() = readX(1) { array.getUInt8(offset) }
|
fun readUInt8() = readX(1) { array.getUInt8(offset) }
|
||||||
fun readUInt16() = readX(2) { array.getUInt16(offset) }
|
fun readUInt16() = readX(2) { array.getUInt16(offset) }
|
||||||
fun readInt32() = readX(4) { array.getInt32(offset) }
|
fun readInt32() = readX(4) { array.getInt32(offset) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,34 +3,34 @@ package jp.juggler.apng.util
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
internal class ByteSequenceQueue(private val bufferRecycler : (ByteSequence) -> Unit) {
|
internal class ByteSequenceQueue(private val bufferRecycler: (ByteSequence) -> Unit) {
|
||||||
|
|
||||||
private val list = LinkedList<ByteSequence>()
|
private val list = LinkedList<ByteSequence>()
|
||||||
|
|
||||||
val remain : Int
|
val remain: Int
|
||||||
get() = list.sumOf { it.length }
|
get() = list.sumOf { it.length }
|
||||||
|
|
||||||
fun add(range : ByteSequence) =list.add(range)
|
fun add(range: ByteSequence) = list.add(range)
|
||||||
|
|
||||||
fun clear() = list.onEach(bufferRecycler).clear()
|
fun clear() = list.onEach(bufferRecycler).clear()
|
||||||
|
|
||||||
fun readBytes(dst : ByteArray, offset : Int, length : Int) : Int {
|
fun readBytes(dst: ByteArray, offset: Int, length: Int): Int {
|
||||||
var dstOffset = offset
|
var dstOffset = offset
|
||||||
var dstRemain = length
|
var dstRemain = length
|
||||||
while(dstRemain > 0 && list.isNotEmpty()) {
|
while (dstRemain > 0 && list.isNotEmpty()) {
|
||||||
val item = list.first()
|
val item = list.first()
|
||||||
if(item.length <= 0) {
|
if (item.length <= 0) {
|
||||||
bufferRecycler(item)
|
bufferRecycler(item)
|
||||||
list.removeFirst()
|
list.removeFirst()
|
||||||
} else {
|
} else {
|
||||||
val delta = min(item.length, dstRemain)
|
val delta = min(item.length, dstRemain)
|
||||||
System.arraycopy(item.array, item.offset, dst, dstOffset, delta)
|
System.arraycopy(item.array, item.offset, dst, dstOffset, delta)
|
||||||
dstOffset += delta
|
dstOffset += delta
|
||||||
dstRemain -= delta
|
dstRemain -= delta
|
||||||
item.offset += delta
|
item.offset += delta
|
||||||
item.length -= delta
|
item.length -= delta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return length - dstRemain
|
return length - dstRemain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,63 +4,63 @@ import jp.juggler.apng.ApngParseError
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.zip.CRC32
|
import java.util.zip.CRC32
|
||||||
|
|
||||||
internal class StreamTokenizer(val inStream : InputStream) {
|
internal class StreamTokenizer(val inStream: InputStream) {
|
||||||
|
|
||||||
fun skipBytes(size : Long) {
|
fun skipBytes(size: Long) {
|
||||||
var nRead = 0L
|
var nRead = 0L
|
||||||
while(true) {
|
while (true) {
|
||||||
val remain = size - nRead
|
val remain = size - nRead
|
||||||
if(remain <= 0) break
|
if (remain <= 0) break
|
||||||
val delta = inStream.skip(size - nRead)
|
val delta = inStream.skip(size - nRead)
|
||||||
if(delta <= 0) throw ApngParseError("skipBytes: unexpected EoS")
|
if (delta <= 0) throw ApngParseError("skipBytes: unexpected EoS")
|
||||||
nRead += delta
|
nRead += delta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readBytes(size : Int) : ByteArray {
|
fun readBytes(size: Int): ByteArray {
|
||||||
val dst = ByteArray(size)
|
val dst = ByteArray(size)
|
||||||
var nRead = 0
|
var nRead = 0
|
||||||
while(true) {
|
while (true) {
|
||||||
val remain = size - nRead
|
val remain = size - nRead
|
||||||
if(remain <= 0) break
|
if (remain <= 0) break
|
||||||
val delta = inStream.read(dst, nRead, size - nRead)
|
val delta = inStream.read(dst, nRead, size - nRead)
|
||||||
if(delta < 0) throw ApngParseError("readBytes: unexpected EoS")
|
if (delta < 0) throw ApngParseError("readBytes: unexpected EoS")
|
||||||
nRead += delta
|
nRead += delta
|
||||||
}
|
}
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readByte() : Int {
|
private fun readByte(): Int {
|
||||||
val b = inStream.read()
|
val b = inStream.read()
|
||||||
if(b == - 1) throw ApngParseError("readByte: unexpected EoS")
|
if (b == -1) throw ApngParseError("readByte: unexpected EoS")
|
||||||
return b and 0xff
|
return b and 0xff
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readInt32() : Int {
|
fun readInt32(): Int {
|
||||||
val b0 = readByte()
|
val b0 = readByte()
|
||||||
val b1 = readByte()
|
val b1 = readByte()
|
||||||
val b2 = readByte()
|
val b2 = readByte()
|
||||||
val b3 = readByte()
|
val b3 = readByte()
|
||||||
|
|
||||||
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
|
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readInt32(crc32 : CRC32) : Int {
|
fun readInt32(crc32: CRC32): Int {
|
||||||
val ba = readBytes(4)
|
val ba = readBytes(4)
|
||||||
crc32.update(ba)
|
crc32.update(ba)
|
||||||
val b0 = ba[0].toInt() and 255
|
val b0 = ba[0].toInt() and 255
|
||||||
val b1 = ba[1].toInt() and 255
|
val b1 = ba[1].toInt() and 255
|
||||||
val b2 = ba[2].toInt() and 255
|
val b2 = ba[2].toInt() and 255
|
||||||
val b3 = ba[3].toInt() and 255
|
val b3 = ba[3].toInt() and 255
|
||||||
|
|
||||||
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
|
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readUInt32() : Long {
|
fun readUInt32(): Long {
|
||||||
val b0 = readByte()
|
val b0 = readByte()
|
||||||
val b1 = readByte()
|
val b1 = readByte()
|
||||||
val b2 = readByte()
|
val b2 = readByte()
|
||||||
val b3 = readByte()
|
val b3 = readByte()
|
||||||
return (b0.toLong() shl 24) or ((b1 shl 16) or (b2 shl 8) or b3).toLong()
|
return (b0.toLong() shl 24) or ((b1 shl 16) or (b2 shl 8) or b3).toLong()
|
||||||
}
|
}
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -135,6 +135,7 @@ dependencies {
|
||||||
implementation project(':apng_android')
|
implementation project(':apng_android')
|
||||||
implementation fileTree(include: ['*.aar'], dir: 'src/main/libs')
|
implementation fileTree(include: ['*.aar'], dir: 'src/main/libs')
|
||||||
|
|
||||||
|
|
||||||
// App1 とサーバ情報カラムで使う
|
// App1 とサーバ情報カラムで使う
|
||||||
api "org.conscrypt:conscrypt-android:2.5.2"
|
api "org.conscrypt:conscrypt-android:2.5.2"
|
||||||
|
|
||||||
|
@ -143,19 +144,48 @@ dependencies {
|
||||||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||||
|
|
||||||
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detekt_version")
|
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detekt_version")
|
||||||
|
|
||||||
|
testImplementation(project(":base"))
|
||||||
|
androidTestImplementation(project(":base"))
|
||||||
|
|
||||||
|
testApi "androidx.arch.core:core-testing:$arch_version"
|
||||||
|
testApi "junit:junit:$junit_version"
|
||||||
|
testApi "org.jetbrains.kotlin:kotlin-test"
|
||||||
|
testApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
|
||||||
|
|
||||||
|
androidTestApi "androidx.test.espresso:espresso-core:3.5.1"
|
||||||
|
androidTestApi "androidx.test.ext:junit-ktx:1.1.5"
|
||||||
|
androidTestApi "androidx.test.ext:junit:1.1.5"
|
||||||
|
androidTestApi "androidx.test.ext:truth:1.5.0"
|
||||||
|
androidTestApi "androidx.test:core-ktx:1.5.0"
|
||||||
|
androidTestApi "androidx.test:core:$androidx_test_version"
|
||||||
|
androidTestApi "androidx.test:runner:1.5.2"
|
||||||
|
androidTestApi "org.jetbrains.kotlin:kotlin-test"
|
||||||
|
androidTestApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
|
||||||
|
|
||||||
|
// To use android test orchestrator
|
||||||
|
androidTestUtil "androidx.test:orchestrator:1.4.2"
|
||||||
|
|
||||||
|
testApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") {
|
||||||
|
exclude group: "com.squareup.okio", module: "okio"
|
||||||
|
exclude group: "com.squareup.okhttp3", module: "okhttp"
|
||||||
|
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-common"
|
||||||
|
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
|
||||||
|
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jdk8"
|
||||||
|
}
|
||||||
|
androidTestApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") {
|
||||||
|
exclude group: "com.squareup.okio", module: "okio"
|
||||||
|
exclude group: "com.squareup.okhttp3", module: "okhttp"
|
||||||
|
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-common"
|
||||||
|
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
|
||||||
|
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jdk8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
// detekt
|
|
||||||
def projectSource = file(projectDir)
|
|
||||||
def configFile = files("$rootDir/config/detekt/config.yml")
|
|
||||||
def baselineFile = file("$rootDir/config/detekt/baseline.xml")
|
|
||||||
def kotlinFiles = "**/*.kt"
|
|
||||||
def resourceFiles = "**/resources/**"
|
|
||||||
def buildFiles = "**/build/**"
|
|
||||||
|
|
||||||
tasks.register("detektAll", Detekt) {
|
tasks.register("detektAll", Detekt) {
|
||||||
description = "Custom DETEKT build for all modules"
|
description = "Custom DETEKT build for all modules"
|
||||||
|
@ -173,12 +203,30 @@ tasks.register("detektAll", Detekt) {
|
||||||
// preconfigure defaults
|
// preconfigure defaults
|
||||||
buildUponDefaultConfig = true
|
buildUponDefaultConfig = true
|
||||||
|
|
||||||
setSource(projectSource)
|
def configFile = files("$rootDir/config/detekt/config.yml")
|
||||||
config.setFrom(configFile)
|
config.setFrom(configFile)
|
||||||
|
|
||||||
|
def baselineFile = file("$rootDir/config/detekt/baseline.xml")
|
||||||
if (baselineFile.isFile()) {
|
if (baselineFile.isFile()) {
|
||||||
baseline.set(baselineFile)
|
baseline.set(baselineFile)
|
||||||
}
|
}
|
||||||
include(kotlinFiles)
|
|
||||||
|
source = files(
|
||||||
|
"$rootDir/apng/src",
|
||||||
|
"$rootDir/apng_android/src",
|
||||||
|
"$rootDir/app/src",
|
||||||
|
"$rootDir/base/src",
|
||||||
|
"$rootDir/colorpicker/src",
|
||||||
|
"$rootDir/emoji/src",
|
||||||
|
"$rootDir/icon_material_symbols/src",
|
||||||
|
"$rootDir/sample_apng/src",
|
||||||
|
)
|
||||||
|
|
||||||
|
// def kotlinFiles = "**/*.kt"
|
||||||
|
// include(kotlinFiles)
|
||||||
|
|
||||||
|
def resourceFiles = "**/resources/**"
|
||||||
|
def buildFiles = "**/build/**"
|
||||||
exclude(resourceFiles, buildFiles)
|
exclude(resourceFiles, buildFiles)
|
||||||
reports {
|
reports {
|
||||||
html.enabled = true
|
html.enabled = true
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package jp.juggler.subwaytooter
|
package jp.juggler.subwaytooter
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package jp.juggler.subwaytooter
|
package jp.juggler.subwaytooter
|
||||||
|
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import jp.juggler.util.jsonArray
|
import jp.juggler.util.data.*
|
||||||
import org.jetbrains.anko.collections.forEachReversedByIndex
|
import org.jetbrains.anko.collections.forEachReversedByIndex
|
||||||
import org.jetbrains.anko.collections.forEachReversedWithIndex
|
import org.jetbrains.anko.collections.forEachReversedWithIndex
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
@ -19,7 +19,7 @@ class JsonArrayForEach {
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun test() {
|
fun test() {
|
||||||
val array = jsonArray {
|
val array = buildJsonArray {
|
||||||
add("a")
|
add("a")
|
||||||
add("b")
|
add("b")
|
||||||
add(null)
|
add(null)
|
||||||
|
@ -59,4 +59,4 @@ class JsonArrayForEach {
|
||||||
|
|
||||||
assertEquals(count, 24)
|
assertEquals(count, 24)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package jp.juggler.subwaytooter
|
package jp.juggler.subwaytooter
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.jrummyapps.android.colorpicker.parseColorString
|
import com.jrummyapps.android.colorpicker.parseColorString
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package jp.juggler.subwaytooter
|
package jp.juggler.subwaytooter
|
||||||
|
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import jp.juggler.subwaytooter.api.entity.TootAccount
|
import jp.juggler.subwaytooter.api.entity.TootAccount
|
||||||
import jp.juggler.util.data.asciiPatternString
|
import jp.juggler.util.data.asciiPatternString
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
|
|
@ -2,7 +2,7 @@ package jp.juggler.subwaytooter
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||||
import jp.juggler.util.asciiRegex
|
import jp.juggler.util.data.asciiRegex
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
@ -1,55 +1,48 @@
|
||||||
package jp.juggler.subwaytooter
|
package jp.juggler.subwaytooter
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.runner.AndroidJUnit4
|
|
||||||
import jp.juggler.subwaytooter.api.TootApiCallback
|
import jp.juggler.subwaytooter.api.TootApiCallback
|
||||||
import jp.juggler.subwaytooter.api.TootApiClient
|
import jp.juggler.subwaytooter.api.TootApiClient
|
||||||
import jp.juggler.subwaytooter.api.entity.Host
|
import jp.juggler.subwaytooter.api.entity.Host
|
||||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
|
import jp.juggler.subwaytooter.testutil.MainDispatcherRule
|
||||||
|
import jp.juggler.subwaytooter.testutil.MockInterceptor
|
||||||
import jp.juggler.subwaytooter.util.SimpleHttpClientImpl
|
import jp.juggler.subwaytooter.util.SimpleHttpClientImpl
|
||||||
import jp.juggler.util.coroutine.AppDispatchers
|
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
import jp.juggler.util.network.MySslSocketFactory
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.coroutines.runBlocking
|
import okhttp3.*
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okhttp3.ConnectionSpec
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class TestTootInstance {
|
class TestTootInstance {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LogCategory("TestTootInstance")
|
private val log = LogCategory("TestTootInstance")
|
||||||
|
}
|
||||||
|
|
||||||
// val cookieJar = JavaNetCookieJar(CookieManager().apply {
|
// テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
|
||||||
// setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
// プロパティは記述順に初期化されることに注意
|
||||||
// CookieHandler.setDefault(this)
|
@get:Rule
|
||||||
// })
|
val mainDispatcherRule = MainDispatcherRule()
|
||||||
|
|
||||||
private val okHttp = OkHttpClient.Builder()
|
private val client by lazy {
|
||||||
.connectTimeout(60.toLong(), TimeUnit.SECONDS)
|
val mockInterceptor = MockInterceptor(
|
||||||
.readTimeout(60.toLong(), TimeUnit.SECONDS)
|
// テストアプリのコンテキスト
|
||||||
.writeTimeout(60.toLong(), TimeUnit.SECONDS)
|
context = InstrumentationRegistry.getInstrumentation().context!!,
|
||||||
.pingInterval(10, TimeUnit.SECONDS)
|
// テストアプリ中のリソースID
|
||||||
.connectionSpecs(
|
rawId = jp.juggler.subwaytooter.test.R.raw.test_toot_instance_mock,
|
||||||
Collections.singletonList(
|
)
|
||||||
ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
|
||||||
.allEnabledCipherSuites()
|
|
||||||
.allEnabledTlsVersions()
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.sslSocketFactory(MySslSocketFactory, MySslSocketFactory.trustManager)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val dummyClientCallback = object : TootApiCallback {
|
val okHttp = OkHttpClient.Builder().addInterceptor(mockInterceptor).build()
|
||||||
|
|
||||||
|
val dummyClientCallback = object : TootApiCallback {
|
||||||
override suspend fun isApiCancelled() = false
|
override suspend fun isApiCancelled() = false
|
||||||
|
|
||||||
override suspend fun publishApiProgress(s: String) {
|
override suspend fun publishApiProgress(s: String) {
|
||||||
|
@ -61,9 +54,8 @@ class TestTootInstance {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val appContext = InstrumentationRegistry.getInstrumentation().targetContext!!
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext!!
|
||||||
|
TootApiClient(
|
||||||
val client = TootApiClient(
|
|
||||||
context = appContext,
|
context = appContext,
|
||||||
httpClient = SimpleHttpClientImpl(appContext, okHttp),
|
httpClient = SimpleHttpClientImpl(appContext, okHttp),
|
||||||
callback = dummyClientCallback
|
callback = dummyClientCallback
|
||||||
|
@ -76,34 +68,26 @@ class TestTootInstance {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testWithoutAccount() {
|
fun instanceByHostname() = runTest {
|
||||||
runBlocking {
|
suspend fun a(host: Host) {
|
||||||
withContext(AppDispatchers.io) {
|
val (ti, ri) = TootInstance.getEx(client, hostArg = host)
|
||||||
suspend fun a(host: Host) {
|
assertNull("no error", ri?.error)
|
||||||
val (ti, ri) = TootInstance.getEx(client, hostArg = host)
|
assertNotNull("instance information", ti)
|
||||||
assertNotNull(ti)
|
ti!!.run { log.d("$instanceType $uri $version") }
|
||||||
assertNull(ri?.error)
|
|
||||||
ti!!.run { log.d("$instanceType $uri $version") }
|
|
||||||
}
|
|
||||||
a(Host.parse("mastodon.juggler.jp"))
|
|
||||||
a(Host.parse("misskey.io"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
a(Host.parse("mastodon.juggler.jp"))
|
||||||
|
a(Host.parse("misskey.io"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testWithAccount() {
|
fun testWithAccount() = runTest {
|
||||||
runBlocking {
|
suspend fun a(account: SavedAccount) {
|
||||||
withContext(AppDispatchers.io) {
|
val (ti, ri) = TootInstance.getEx(client, account = account)
|
||||||
suspend fun a(account: SavedAccount) {
|
assertNull(ri?.error)
|
||||||
val (ti, ri) = TootInstance.getEx(client, account = account)
|
assertNotNull(ti)
|
||||||
assertNull(ri?.error)
|
ti!!.run { log.d("${account.acct} $instanceType $uri $version") }
|
||||||
assertNotNull(ti)
|
|
||||||
ti!!.run { log.d("${account.acct} $instanceType $uri $version") }
|
|
||||||
}
|
|
||||||
a(SavedAccount(45, "tateisu@mastodon.juggler.jp"))
|
|
||||||
a(SavedAccount(45, "tateisu@misskey.io", misskeyVersion = 12))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
a(SavedAccount(45, "tateisu@mastodon.juggler.jp"))
|
||||||
|
a(SavedAccount(45, "tateisu@misskey.io", misskeyVersion = 12))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package jp.juggler.subwaytooter
|
package jp.juggler.subwaytooter
|
||||||
|
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import jp.juggler.util.data.CharacterGroup
|
import jp.juggler.util.data.CharacterGroup
|
||||||
import jp.juggler.util.data.WordTrieTree
|
import jp.juggler.util.data.WordTrieTree
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package jp.juggler.subwaytooter.api
|
package jp.juggler.subwaytooter.api
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import jp.juggler.subwaytooter.api.entity.*
|
import jp.juggler.subwaytooter.api.entity.*
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.util.data.*
|
import jp.juggler.util.data.*
|
||||||
|
|
|
@ -2,18 +2,19 @@
|
||||||
|
|
||||||
package jp.juggler.subwaytooter.api
|
package jp.juggler.subwaytooter.api
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.runner.AndroidJUnit4
|
|
||||||
import jp.juggler.subwaytooter.api.entity.Host
|
import jp.juggler.subwaytooter.api.entity.Host
|
||||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
|
import jp.juggler.subwaytooter.testutil.MainDispatcherRule
|
||||||
import jp.juggler.subwaytooter.util.SimpleHttpClient
|
import jp.juggler.subwaytooter.util.SimpleHttpClient
|
||||||
import jp.juggler.util.data.JsonObject
|
import jp.juggler.util.data.JsonObject
|
||||||
import jp.juggler.util.data.buildJsonArray
|
import jp.juggler.util.data.buildJsonArray
|
||||||
import jp.juggler.util.data.buildJsonObject
|
import jp.juggler.util.data.buildJsonObject
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
import jp.juggler.util.network.MEDIA_TYPE_JSON
|
import jp.juggler.util.network.MEDIA_TYPE_JSON
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.test.runTest
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
@ -21,6 +22,7 @@ import okio.Buffer
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import okio.ByteString
|
import okio.ByteString
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
@ -28,6 +30,12 @@ import java.util.concurrent.atomic.AtomicReference
|
||||||
@Suppress("MemberVisibilityCanPrivate")
|
@Suppress("MemberVisibilityCanPrivate")
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class TestTootApiClient {
|
class TestTootApiClient {
|
||||||
|
|
||||||
|
// テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
|
||||||
|
// プロパティは記述順に初期化されることに注意
|
||||||
|
@get:Rule
|
||||||
|
val mainDispatcherRule = MainDispatcherRule()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LogCategory("TestTootApiClient")
|
private val log = LogCategory("TestTootApiClient")
|
||||||
}
|
}
|
||||||
|
@ -229,14 +237,21 @@ class TestTootApiClient {
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
else ->
|
"/api/meta" -> Response.Builder()
|
||||||
Response.Builder()
|
.request(request)
|
||||||
.request(request)
|
.protocol(Protocol.HTTP_1_1)
|
||||||
.protocol(Protocol.HTTP_1_1)
|
.code(404)
|
||||||
.code(200)
|
.message("not found")
|
||||||
.message("status-message")
|
.body("""{"error":"404 not found"}""".toResponseBody(MEDIA_TYPE_JSON))
|
||||||
.body(request.url.toString().toResponseBody(mediaTypeTextPlain))
|
.build()
|
||||||
.build()
|
|
||||||
|
else -> Response.Builder()
|
||||||
|
.request(request)
|
||||||
|
.protocol(Protocol.HTTP_1_1)
|
||||||
|
.code(200)
|
||||||
|
.message("status-message")
|
||||||
|
.body(request.url.toString().toResponseBody(mediaTypeTextPlain))
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -519,7 +534,7 @@ class TestTootApiClient {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testIsApiCancelled() {
|
fun testIsApiCancelled() {
|
||||||
runBlocking {
|
runTest {
|
||||||
var flag = 0
|
var flag = 0
|
||||||
var progressString: String? = null
|
var progressString: String? = null
|
||||||
var progressValue: Int? = null
|
var progressValue: Int? = null
|
||||||
|
@ -559,7 +574,7 @@ class TestTootApiClient {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSendRequest() {
|
fun testSendRequest() {
|
||||||
runBlocking {
|
runTest {
|
||||||
|
|
||||||
val callback = ProgressRecordTootApiCallback()
|
val callback = ProgressRecordTootApiCallback()
|
||||||
|
|
||||||
|
@ -637,7 +652,7 @@ class TestTootApiClient {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testReadBodyString() {
|
fun testReadBodyString() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val callback = ProgressRecordTootApiCallback()
|
val callback = ProgressRecordTootApiCallback()
|
||||||
val client = TootApiClient(
|
val client = TootApiClient(
|
||||||
appContext,
|
appContext,
|
||||||
|
@ -745,7 +760,7 @@ class TestTootApiClient {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testParseString() {
|
fun testParseString() {
|
||||||
runBlocking {
|
runTest {
|
||||||
|
|
||||||
val callback = ProgressRecordTootApiCallback()
|
val callback = ProgressRecordTootApiCallback()
|
||||||
val client = TootApiClient(
|
val client = TootApiClient(
|
||||||
|
@ -865,7 +880,7 @@ class TestTootApiClient {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testParseJson() {
|
fun testParseJson() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val callback = ProgressRecordTootApiCallback()
|
val callback = ProgressRecordTootApiCallback()
|
||||||
val client = TootApiClient(
|
val client = TootApiClient(
|
||||||
appContext,
|
appContext,
|
||||||
|
@ -1046,7 +1061,7 @@ class TestTootApiClient {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRegisterClient() {
|
fun testRegisterClient() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val callback = ProgressRecordTootApiCallback()
|
val callback = ProgressRecordTootApiCallback()
|
||||||
val client = TootApiClient(
|
val client = TootApiClient(
|
||||||
appContext,
|
appContext,
|
||||||
|
@ -1064,7 +1079,7 @@ class TestTootApiClient {
|
||||||
assertEquals(null, result?.error)
|
assertEquals(null, result?.error)
|
||||||
var jsonObject = result?.jsonObject
|
var jsonObject = result?.jsonObject
|
||||||
assertNotNull(jsonObject)
|
assertNotNull(jsonObject)
|
||||||
if (jsonObject == null) return@runBlocking
|
if (jsonObject == null) return@runTest
|
||||||
val clientInfo = jsonObject
|
val clientInfo = jsonObject
|
||||||
|
|
||||||
// clientCredential の作成
|
// clientCredential の作成
|
||||||
|
@ -1073,7 +1088,7 @@ class TestTootApiClient {
|
||||||
assertEquals(null, result?.error)
|
assertEquals(null, result?.error)
|
||||||
val clientCredential = result?.string
|
val clientCredential = result?.string
|
||||||
assertNotNull(clientCredential)
|
assertNotNull(clientCredential)
|
||||||
if (clientCredential == null) return@runBlocking
|
if (clientCredential == null) return@runTest
|
||||||
clientInfo[TootApiClient.KEY_CLIENT_CREDENTIAL] = clientCredential
|
clientInfo[TootApiClient.KEY_CLIENT_CREDENTIAL] = clientCredential
|
||||||
|
|
||||||
// clientCredential の検証
|
// clientCredential の検証
|
||||||
|
@ -1082,7 +1097,7 @@ class TestTootApiClient {
|
||||||
assertEquals(null, result?.error)
|
assertEquals(null, result?.error)
|
||||||
jsonObject = result?.jsonObject
|
jsonObject = result?.jsonObject
|
||||||
assertNotNull(jsonObject) // 中味は別に見てない。jsonObjectなら良いらしい
|
assertNotNull(jsonObject) // 中味は別に見てない。jsonObjectなら良いらしい
|
||||||
if (jsonObject == null) return@runBlocking
|
if (jsonObject == null) return@runTest
|
||||||
|
|
||||||
var url: String?
|
var url: String?
|
||||||
|
|
||||||
|
@ -1095,7 +1110,7 @@ class TestTootApiClient {
|
||||||
result = client.authentication1(clientName)
|
result = client.authentication1(clientName)
|
||||||
url = result?.string
|
url = result?.string
|
||||||
assertNotNull(url)
|
assertNotNull(url)
|
||||||
if (url == null) return@runBlocking
|
if (url == null) return@runTest
|
||||||
println(url)
|
println(url)
|
||||||
|
|
||||||
// ブラウザからコールバックで受け取ったcodeを処理する
|
// ブラウザからコールバックで受け取ったcodeを処理する
|
||||||
|
@ -1103,29 +1118,29 @@ class TestTootApiClient {
|
||||||
result = client.authentication2Mastodon(clientName, "DUMMY_CODE", refToken)
|
result = client.authentication2Mastodon(clientName, "DUMMY_CODE", refToken)
|
||||||
jsonObject = result?.jsonObject
|
jsonObject = result?.jsonObject
|
||||||
assertNotNull(jsonObject)
|
assertNotNull(jsonObject)
|
||||||
if (jsonObject == null) return@runBlocking
|
if (jsonObject == null) return@runTest
|
||||||
println(jsonObject.toString())
|
println(jsonObject.toString())
|
||||||
|
|
||||||
// 認証できたならアクセストークンがある
|
// 認証できたならアクセストークンがある
|
||||||
val tokenInfo = result?.tokenInfo
|
val tokenInfo = result?.tokenInfo
|
||||||
assertNotNull(tokenInfo)
|
assertNotNull(tokenInfo)
|
||||||
if (tokenInfo == null) return@runBlocking
|
if (tokenInfo == null) return@runTest
|
||||||
val accessToken = tokenInfo.string("access_token")
|
val accessToken = tokenInfo.string("access_token")
|
||||||
assertNotNull(accessToken)
|
assertNotNull(accessToken)
|
||||||
if (accessToken == null) return@runBlocking
|
if (accessToken == null) return@runTest
|
||||||
|
|
||||||
// アカウント手動入力でログインする場合はこの関数を直接呼び出す
|
// アカウント手動入力でログインする場合はこの関数を直接呼び出す
|
||||||
result = client.getUserCredential(accessToken, tokenInfo)
|
result = client.getUserCredential(accessToken, tokenInfo)
|
||||||
jsonObject = result?.jsonObject
|
jsonObject = result?.jsonObject
|
||||||
assertNotNull(jsonObject)
|
assertNotNull(jsonObject)
|
||||||
if (jsonObject == null) return@runBlocking
|
if (jsonObject == null) return@runTest
|
||||||
println(jsonObject.toString())
|
println(jsonObject.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testGetInstanceInformation() {
|
fun testGetInstanceInformation() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val callback = ProgressRecordTootApiCallback()
|
val callback = ProgressRecordTootApiCallback()
|
||||||
val client = TootApiClient(
|
val client = TootApiClient(
|
||||||
appContext,
|
appContext,
|
||||||
|
@ -1135,8 +1150,8 @@ class TestTootApiClient {
|
||||||
val instance = Host.parse("unit-test")
|
val instance = Host.parse("unit-test")
|
||||||
client.apiHost = instance
|
client.apiHost = instance
|
||||||
val (instanceInfo, instanceResult) = TootInstance.get(client)
|
val (instanceInfo, instanceResult) = TootInstance.get(client)
|
||||||
assertNotNull(instanceInfo)
|
assertNull("no error", instanceResult?.error)
|
||||||
assertNotNull(instanceResult)
|
assertNotNull("instance info", instanceInfo)
|
||||||
val json = instanceResult?.jsonObject
|
val json = instanceResult?.jsonObject
|
||||||
if (json != null) println(json.toString())
|
if (json != null) println(json.toString())
|
||||||
}
|
}
|
||||||
|
@ -1144,7 +1159,7 @@ class TestTootApiClient {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testGetHttp() {
|
fun testGetHttp() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val callback = ProgressRecordTootApiCallback()
|
val callback = ProgressRecordTootApiCallback()
|
||||||
val client = TootApiClient(
|
val client = TootApiClient(
|
||||||
appContext,
|
appContext,
|
||||||
|
@ -1160,7 +1175,7 @@ class TestTootApiClient {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRequest() {
|
fun testRequest() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val tokenInfo = JsonObject()
|
val tokenInfo = JsonObject()
|
||||||
tokenInfo["access_token"] = "DUMMY_ACCESS_TOKEN"
|
tokenInfo["access_token"] = "DUMMY_ACCESS_TOKEN"
|
||||||
|
|
||||||
|
@ -1188,7 +1203,7 @@ class TestTootApiClient {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testWebSocket() {
|
fun testWebSocket() {
|
||||||
runBlocking {
|
runTest {
|
||||||
val tokenInfo = buildJsonObject {
|
val tokenInfo = buildJsonObject {
|
||||||
put("access_token", "DUMMY_ACCESS_TOKEN")
|
put("access_token", "DUMMY_ACCESS_TOKEN")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package jp.juggler.subwaytooter.api.entity
|
package jp.juggler.subwaytooter.api.entity
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
import jp.juggler.subwaytooter.api.TootParser
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.util.data.*
|
import jp.juggler.util.data.*
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package jp.juggler.subwaytooter.api.entity
|
package jp.juggler.subwaytooter.api.entity
|
||||||
|
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import jp.juggler.subwaytooter.util.LinkHelper
|
import jp.juggler.subwaytooter.util.LinkHelper
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package jp.juggler.subwaytooter.database
|
package jp.juggler.subwaytooter.database
|
||||||
|
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
|
@ -4,7 +4,7 @@ import android.content.Context
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import jp.juggler.subwaytooter.global.DB_VERSION
|
import jp.juggler.subwaytooter.global.DB_VERSION
|
||||||
import jp.juggler.subwaytooter.global.TABLE_LIST
|
import jp.juggler.subwaytooter.global.TABLE_LIST
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package jp.juggler.subwaytooter.util
|
package jp.juggler.subwaytooter.util
|
||||||
|
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package jp.juggler.subwaytooter.util
|
package jp.juggler.subwaytooter.util
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import jp.juggler.subwaytooter.api.entity.Host
|
import jp.juggler.subwaytooter.api.entity.Host
|
||||||
import jp.juggler.util.neatSpaces
|
import jp.juggler.util.data.*
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
@ -17,7 +17,7 @@ class TestHtmlDecoder {
|
||||||
val start: Int,
|
val start: Int,
|
||||||
val end: Int,
|
val end: Int,
|
||||||
val flags: Int,
|
val flags: Int,
|
||||||
val text: String
|
val text: String,
|
||||||
) {
|
) {
|
||||||
override fun toString() = "[$start..$end) $flags ${span.javaClass.simpleName} $text"
|
override fun toString() = "[$start..$end) $flags ${span.javaClass.simpleName} $text"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,6 @@ import jp.juggler.util.log.LogCategory
|
||||||
import jp.juggler.util.log.showToast
|
import jp.juggler.util.log.showToast
|
||||||
import jp.juggler.util.network.toFormRequestBody
|
import jp.juggler.util.network.toFormRequestBody
|
||||||
import jp.juggler.util.network.toPost
|
import jp.juggler.util.network.toPost
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
private val log = LogCategory("Action_Tag")
|
private val log = LogCategory("Action_Tag")
|
||||||
|
|
|
@ -43,7 +43,6 @@ import jp.juggler.util.log.showToast
|
||||||
import jp.juggler.util.ui.activity
|
import jp.juggler.util.ui.activity
|
||||||
import jp.juggler.util.ui.attrColor
|
import jp.juggler.util.ui.attrColor
|
||||||
import jp.juggler.util.ui.createColoredDrawable
|
import jp.juggler.util.ui.createColoredDrawable
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jetbrains.anko.backgroundColor
|
import org.jetbrains.anko.backgroundColor
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
|
@ -8,6 +8,7 @@ import jp.juggler.subwaytooter.pref.PrefB
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.subwaytooter.util.LinkHelper
|
import jp.juggler.subwaytooter.util.LinkHelper
|
||||||
import jp.juggler.subwaytooter.util.VersionString
|
import jp.juggler.subwaytooter.util.VersionString
|
||||||
|
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
|
||||||
import jp.juggler.util.coroutine.launchDefault
|
import jp.juggler.util.coroutine.launchDefault
|
||||||
import jp.juggler.util.data.*
|
import jp.juggler.util.data.*
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
|
@ -15,7 +16,6 @@ import jp.juggler.util.log.withCaption
|
||||||
import jp.juggler.util.network.toPostRequestBuilder
|
import jp.juggler.util.network.toPostRequestBuilder
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withTimeout
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
|
@ -431,7 +431,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
|
||||||
try {
|
try {
|
||||||
val req = requestQueue.receive()
|
val req = requestQueue.receive()
|
||||||
val r = try {
|
val r = try {
|
||||||
withTimeout(30000L) {
|
withTimeoutSafe(30000L) {
|
||||||
handleRequest(req)
|
handleRequest(req)
|
||||||
}
|
}
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
|
@ -481,7 +481,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
|
||||||
val cacheEntry = (hostArg ?: account?.apiHost ?: client.apiHost)?.getCacheEntry()
|
val cacheEntry = (hostArg ?: account?.apiHost ?: client.apiHost)?.getCacheEntry()
|
||||||
?: return tiError("missing host.")
|
?: return tiError("missing host.")
|
||||||
|
|
||||||
return withTimeout(30000L) {
|
return withTimeoutSafe(30000L) {
|
||||||
suspendCoroutine { cont ->
|
suspendCoroutine { cont ->
|
||||||
QueuedRequest(cont, allowPixelfed) { cached ->
|
QueuedRequest(cont, allowPixelfed) { cached ->
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,11 @@ import android.net.wifi.WifiManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import jp.juggler.subwaytooter.App1
|
import jp.juggler.subwaytooter.App1
|
||||||
|
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
|
||||||
import jp.juggler.util.log.*
|
import jp.juggler.util.log.*
|
||||||
import jp.juggler.util.systemService
|
import jp.juggler.util.systemService
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withTimeout
|
|
||||||
|
|
||||||
class CheckerWakeLocks(contextArg: Context) {
|
class CheckerWakeLocks(contextArg: Context) {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -118,7 +118,7 @@ class CheckerWakeLocks(contextArg: Context) {
|
||||||
suspend fun checkConnection() {
|
suspend fun checkConnection() {
|
||||||
var connectionState: String? = null
|
var connectionState: String? = null
|
||||||
try {
|
try {
|
||||||
withTimeout(10000L) {
|
withTimeoutSafe(10000L) {
|
||||||
while (true) {
|
while (true) {
|
||||||
connectionState = appState.networkTracker.connectionState
|
connectionState = appState.networkTracker.connectionState
|
||||||
?: break // null if connected
|
?: break // null if connected
|
||||||
|
|
|
@ -29,7 +29,6 @@ import jp.juggler.util.network.toPostRequestBuilder
|
||||||
import jp.juggler.util.network.toPut
|
import jp.juggler.util.network.toPut
|
||||||
import jp.juggler.util.network.toPutRequestBuilder
|
import jp.juggler.util.network.toPutRequestBuilder
|
||||||
import jp.juggler.util.ui.*
|
import jp.juggler.util.ui.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
|
@ -21,7 +21,6 @@ import jp.juggler.util.network.MEDIA_TYPE_JSON
|
||||||
import jp.juggler.util.network.toPostRequestBuilder
|
import jp.juggler.util.network.toPostRequestBuilder
|
||||||
import jp.juggler.util.ui.*
|
import jp.juggler.util.ui.*
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package jp.juggler.subwaytooter.util
|
package jp.juggler.subwaytooter.util
|
||||||
|
|
||||||
|
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
|
||||||
import jp.juggler.util.coroutine.launchDefault
|
import jp.juggler.util.coroutine.launchDefault
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.withTimeout
|
|
||||||
|
|
||||||
abstract class WorkerBase(
|
abstract class WorkerBase(
|
||||||
private val waiter: Channel<Unit> = Channel(capacity = Channel.CONFLATED),
|
private val waiter: Channel<Unit> = Channel(capacity = Channel.CONFLATED),
|
||||||
|
@ -17,7 +17,7 @@ abstract class WorkerBase(
|
||||||
abstract suspend fun run()
|
abstract suspend fun run()
|
||||||
|
|
||||||
suspend fun waitEx(ms: Long) = try {
|
suspend fun waitEx(ms: Long) = try {
|
||||||
withTimeout(ms) { waiter.receive() }
|
withTimeoutSafe(ms) { waiter.receive() }
|
||||||
} catch (ignored: TimeoutCancellationException) {
|
} catch (ignored: TimeoutCancellationException) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,11 +111,18 @@ dependencies {
|
||||||
testApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
|
testApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
|
||||||
|
|
||||||
androidTestApi "androidx.test.espresso:espresso-core:3.5.1"
|
androidTestApi "androidx.test.espresso:espresso-core:3.5.1"
|
||||||
|
androidTestApi "androidx.test.ext:junit-ktx:1.1.5"
|
||||||
androidTestApi "androidx.test.ext:junit:1.1.5"
|
androidTestApi "androidx.test.ext:junit:1.1.5"
|
||||||
|
androidTestApi "androidx.test.ext:truth:1.5.0"
|
||||||
|
androidTestApi "androidx.test:core-ktx:1.5.0"
|
||||||
androidTestApi "androidx.test:core:$androidx_test_version"
|
androidTestApi "androidx.test:core:$androidx_test_version"
|
||||||
|
androidTestApi "androidx.test:runner:1.5.2"
|
||||||
androidTestApi "org.jetbrains.kotlin:kotlin-test"
|
androidTestApi "org.jetbrains.kotlin:kotlin-test"
|
||||||
androidTestApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
|
androidTestApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
|
||||||
|
|
||||||
|
// To use android test orchestrator
|
||||||
|
androidTestUtil "androidx.test:orchestrator:1.4.2"
|
||||||
|
|
||||||
testApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") {
|
testApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") {
|
||||||
exclude group: "com.squareup.okio", module: "okio"
|
exclude group: "com.squareup.okio", module: "okio"
|
||||||
exclude group: "com.squareup.okhttp3", module: "okhttp"
|
exclude group: "com.squareup.okhttp3", module: "okhttp"
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
package jp.juggler.base
|
package jp.juggler.base
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import jp.juggler.base.JugglerBase.Companion.jugglerBase
|
import jp.juggler.base.JugglerBase.Companion.jugglerBase
|
||||||
import jp.juggler.base.JugglerBase.Companion.jugglerBaseNullable
|
import jp.juggler.base.JugglerBase.Companion.jugglerBaseNullable
|
||||||
import jp.juggler.base.JugglerBase.Companion.prepareJugglerBase
|
import jp.juggler.base.JugglerBase.Companion.prepareJugglerBase
|
||||||
|
import org.junit.Assert.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
@ -23,6 +21,6 @@ class JugglerBaseTest {
|
||||||
assertNotNull("JubblerBase is initialized for a test.", jugglerBaseNullable)
|
assertNotNull("JubblerBase is initialized for a test.", jugglerBaseNullable)
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
appContext.prepareJugglerBase
|
appContext.prepareJugglerBase
|
||||||
assertNotNull( "JubblerBase is initialized after prepare.",jugglerBase)
|
assertNotNull("JubblerBase is initialized after prepare.", jugglerBase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package jp.juggler.util.coroutine
|
package jp.juggler.util.coroutine
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test時にdispatcherを差し替えられるようにする
|
* Test時にdispatcherを差し替えられるようにする
|
||||||
|
@ -33,4 +32,15 @@ object AppDispatchers {
|
||||||
default = testDispatcher
|
default = testDispatcher
|
||||||
io = testDispatcher
|
io = testDispatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* withTimeout はrunTest内部だと即座に例外を出すので、
|
||||||
|
* テスト中はタイムアウトをチェックしないようにする
|
||||||
|
* https://stackoverflow.com/questions/70658926/how-to-use-kotlinx-coroutines-withtimeout-in-kotlinx-coroutines-test-runtest
|
||||||
|
*/
|
||||||
|
suspend fun <T> withTimeoutSafe(timeMillis: Long, block: suspend CoroutineScope.() -> T) =
|
||||||
|
when (io) {
|
||||||
|
Dispatchers.IO -> withTimeout(timeMillis, block)
|
||||||
|
else -> coroutineScope { block() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package jp.juggler.util.coroutine
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package jp.juggler.util.data
|
package jp.juggler.util.data
|
||||||
|
|
||||||
import java.util.LinkedHashMap
|
|
||||||
|
|
||||||
// same as x?.let{ dst.add(it) }
|
// same as x?.let{ dst.add(it) }
|
||||||
fun <T> T.addTo(dst: ArrayList<T>) = dst.add(this)
|
fun <T> T.addTo(dst: ArrayList<T>) = dst.add(this)
|
||||||
|
|
||||||
|
@ -34,4 +32,3 @@ fun <T : Any> MutableCollection<T>.removeFirst(check: (T) -> Boolean): T? {
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,8 +59,8 @@ fun Cursor.getBlobOrNull(keyIdx: Int) =
|
||||||
fun Cursor.getBlobOrNull(key: String) =
|
fun Cursor.getBlobOrNull(key: String) =
|
||||||
getBlobOrNull(getColumnIndex(key))
|
getBlobOrNull(getColumnIndex(key))
|
||||||
|
|
||||||
fun Cursor.columnIndexOrThrow(key:String)=
|
fun Cursor.columnIndexOrThrow(key: String) =
|
||||||
getColumnIndex(key).takeIf{it>=0L} ?: error("missing column $key")
|
getColumnIndex(key).takeIf { it >= 0L } ?: error("missing column $key")
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,6 @@ import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.annotation.RawRes
|
import androidx.annotation.RawRes
|
||||||
import jp.juggler.util.data.getStringOrNull
|
|
||||||
import jp.juggler.util.data.notEmpty
|
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
import okhttp3.internal.closeQuietly
|
import okhttp3.internal.closeQuietly
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
|
@ -69,7 +69,7 @@ class WordTrieTree {
|
||||||
// 単語の追加
|
// 単語の追加
|
||||||
fun add(
|
fun add(
|
||||||
s: String,
|
s: String,
|
||||||
tag:Any?=null,
|
tag: Any? = null,
|
||||||
validator: (src: CharSequence, start: Int, end: Int) -> Boolean = EMPTY_VALIDATOR,
|
validator: (src: CharSequence, start: Int, end: Int) -> Boolean = EMPTY_VALIDATOR,
|
||||||
) {
|
) {
|
||||||
val t = CharacterGroup.Tokenizer().reset(s, 0, s.length)
|
val t = CharacterGroup.Tokenizer().reset(s, 0, s.length)
|
||||||
|
@ -92,9 +92,9 @@ class WordTrieTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
// タグを覚える
|
// タグを覚える
|
||||||
if(tag!=null){
|
if (tag != null) {
|
||||||
val tags = node.matchTags
|
val tags = node.matchTags
|
||||||
?: ArrayList<Any>().also{ node.matchTags = it}
|
?: ArrayList<Any>().also { node.matchTags = it }
|
||||||
tags.add(tag)
|
tags.add(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ class WordTrieTree {
|
||||||
if (matchWord != null && node.validator(t.text, start, t.offset)) {
|
if (matchWord != null && node.validator(t.text, start, t.offset)) {
|
||||||
|
|
||||||
// マッチしたことを覚えておく
|
// マッチしたことを覚えておく
|
||||||
dst = Match(start, t.offset, matchWord ,node.matchTags)
|
dst = Match(start, t.offset, matchWord, node.matchTags)
|
||||||
|
|
||||||
// ミュート用途の場合、ひとつでも単語にマッチすればより長い探索は必要ない
|
// ミュート用途の場合、ひとつでも単語にマッチすればより長い探索は必要ない
|
||||||
if (allowShortMatch) break
|
if (allowShortMatch) break
|
||||||
|
@ -205,7 +205,7 @@ class WordTrieTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
@OptIn(ExperimentalContracts::class)
|
||||||
fun WordTrieTree?.isNullOrEmpty() :Boolean {
|
fun WordTrieTree?.isNullOrEmpty(): Boolean {
|
||||||
contract {
|
contract {
|
||||||
returns(false) implies (this@isNullOrEmpty != null)
|
returns(false) implies (this@isNullOrEmpty != null)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,11 @@ import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
|
||||||
import jp.juggler.util.coroutine.runOnMainLooper
|
import jp.juggler.util.coroutine.runOnMainLooper
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withTimeout
|
|
||||||
import me.drakeet.support.toast.ToastCompat
|
import me.drakeet.support.toast.ToastCompat
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
@ -84,7 +84,7 @@ fun initializeToastUtils(app: Application) {
|
||||||
*/
|
*/
|
||||||
suspend fun Animation.startAndAwait(duration: Long, v: View) =
|
suspend fun Animation.startAndAwait(duration: Long, v: View) =
|
||||||
try {
|
try {
|
||||||
withTimeout(duration + 333L) {
|
withTimeoutSafe(duration + 333L) {
|
||||||
suspendCancellableCoroutine { cont ->
|
suspendCancellableCoroutine { cont ->
|
||||||
v.clearAnimation()
|
v.clearAnimation()
|
||||||
this@startAndAwait.duration = duration
|
this@startAndAwait.duration = duration
|
||||||
|
|
|
@ -37,7 +37,6 @@ internal class AlphaPatternDrawable(private val rectangleSize: Int) : Drawable()
|
||||||
*/
|
*/
|
||||||
private var bitmap: Bitmap? = null
|
private var bitmap: Bitmap? = null
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will generate a bitmap with the pattern as big as the rectangle we were allow to draw on.
|
* This will generate a bitmap with the pattern as big as the rectangle we were allow to draw on.
|
||||||
* We do this to chache the bitmap so we don't need to recreate it each time draw() is called since it takes a few milliseconds
|
* We do this to chache the bitmap so we don't need to recreate it each time draw() is called since it takes a few milliseconds
|
||||||
|
|
|
@ -28,7 +28,7 @@ internal class ColorPaletteAdapter(
|
||||||
val colors: IntArray,
|
val colors: IntArray,
|
||||||
var selectedPosition: Int,
|
var selectedPosition: Int,
|
||||||
@ColorShape val colorShape: Int,
|
@ColorShape val colorShape: Int,
|
||||||
val listener: (Int)->Unit
|
val listener: (Int) -> Unit,
|
||||||
) : BaseAdapter() {
|
) : BaseAdapter() {
|
||||||
|
|
||||||
override fun getCount(): Int = colors.size
|
override fun getCount(): Int = colors.size
|
||||||
|
|
|
@ -37,7 +37,7 @@ import androidx.core.view.ViewCompat
|
||||||
class ColorPanelView @JvmOverloads constructor(
|
class ColorPanelView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyle: Int = 0
|
defStyle: Int = 0,
|
||||||
) : View(context, attrs, defStyle) {
|
) : View(context, attrs, defStyle) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -97,7 +97,6 @@ class ColorPanelView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView)
|
val a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView)
|
||||||
shape = a.getInt(R.styleable.ColorPanelView_cpv_colorShape, ColorShape.CIRCLE)
|
shape = a.getInt(R.styleable.ColorPanelView_cpv_colorShape, ColorShape.CIRCLE)
|
||||||
|
@ -117,11 +116,8 @@ class ColorPanelView @JvmOverloads constructor(
|
||||||
borderColor = typedArray.getColor(0, borderColor)
|
borderColor = typedArray.getColor(0, borderColor)
|
||||||
typedArray.recycle()
|
typedArray.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public override fun onSaveInstanceState(): Parcelable {
|
public override fun onSaveInstanceState(): Parcelable {
|
||||||
val state = Bundle()
|
val state = Bundle()
|
||||||
state.putParcelable("instanceState", super.onSaveInstanceState())
|
state.putParcelable("instanceState", super.onSaveInstanceState())
|
||||||
|
@ -138,7 +134,6 @@ class ColorPanelView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
borderPaint.color = borderColor
|
borderPaint.color = borderColor
|
||||||
colorPaint.color = color
|
colorPaint.color = color
|
||||||
|
@ -236,7 +231,6 @@ class ColorPanelView @JvmOverloads constructor(
|
||||||
alphaPattern.setBounds(left, top, right, bottom)
|
alphaPattern.setBounds(left, top, right, bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the original color. This is only used for previewing colors.
|
* Set the original color. This is only used for previewing colors.
|
||||||
*
|
*
|
||||||
|
@ -264,13 +258,12 @@ class ColorPanelView @JvmOverloads constructor(
|
||||||
val screenWidth = context.resources.displayMetrics.widthPixels
|
val screenWidth = context.resources.displayMetrics.widthPixels
|
||||||
referenceX = screenWidth - referenceX // mirror
|
referenceX = screenWidth - referenceX // mirror
|
||||||
}
|
}
|
||||||
val hint = StringBuilder("#")
|
val hexText = when {
|
||||||
if (Color.alpha(color) != 255) {
|
Color.alpha(color) == 255 -> "%06X".format(color and 0xFFFFFF)
|
||||||
hint.append(Integer.toHexString(color).uppercase())
|
else -> Integer.toHexString(color)
|
||||||
} else {
|
|
||||||
hint.append(String.format("%06X", 0xFFFFFF and color).uppercase())
|
|
||||||
}
|
}
|
||||||
val cheatSheet = Toast.makeText(context, hint.toString(), Toast.LENGTH_SHORT)
|
val hint = "#${hexText.uppercase()}"
|
||||||
|
val cheatSheet = Toast.makeText(context, hint, Toast.LENGTH_SHORT)
|
||||||
if (midy < displayFrame.height()) {
|
if (midy < displayFrame.height()) {
|
||||||
// Show along the top; follow action buttons
|
// Show along the top; follow action buttons
|
||||||
cheatSheet.setGravity(
|
cheatSheet.setGravity(
|
||||||
|
|
|
@ -265,11 +265,10 @@ class ColorPickerDialog :
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// region Custom Picker
|
// region Custom Picker
|
||||||
private fun createPickerView(): View {
|
private fun createPickerView(): View {
|
||||||
val args = arguments ?: throw RuntimeException("createPickerView: args is null")
|
val args = arguments ?: error("createPickerView: args is null")
|
||||||
val activity = activity ?: throw RuntimeException("createPickerView: activity is null")
|
val activity = activity ?: error("createPickerView: activity is null")
|
||||||
val contentView = View.inflate(getActivity(), R.layout.cpv_dialog_color_picker, null)
|
val contentView = View.inflate(getActivity(), R.layout.cpv_dialog_color_picker, null)
|
||||||
colorPicker = contentView.findViewById(R.id.cpv_color_picker_view)
|
colorPicker = contentView.findViewById(R.id.cpv_color_picker_view)
|
||||||
val oldColorPanel: ColorPanelView = contentView.findViewById(R.id.cpv_color_panel_old)
|
val oldColorPanel: ColorPanelView = contentView.findViewById(R.id.cpv_color_panel_old)
|
||||||
|
@ -376,14 +375,13 @@ class ColorPickerDialog :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setHex(color: Int) {
|
private fun setHex(color: Int) {
|
||||||
if (showAlphaSlider) {
|
val hexText = when {
|
||||||
hexEditText!!.setText(String.format("%08X", color))
|
showAlphaSlider -> "%08X".format(color)
|
||||||
} else {
|
else -> "%06X".format(color and 0xFFFFFF)
|
||||||
hexEditText!!.setText(String.format("%06X", 0xFFFFFF and color))
|
|
||||||
}
|
}
|
||||||
|
hexEditText?.setText(hexText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// -- endregion --
|
// -- endregion --
|
||||||
// region Presets Picker
|
// region Presets Picker
|
||||||
private fun createPresetsView(): View {
|
private fun createPresetsView(): View {
|
||||||
|
@ -399,13 +397,13 @@ class ColorPickerDialog :
|
||||||
shadesLayout?.visibility = View.GONE
|
shadesLayout?.visibility = View.GONE
|
||||||
contentView.findViewById<View>(R.id.shades_divider).visibility = View.GONE
|
contentView.findViewById<View>(R.id.shades_divider).visibility = View.GONE
|
||||||
}
|
}
|
||||||
adapter = ColorPaletteAdapter(presets, selectedItemPosition, colorShape){
|
adapter = ColorPaletteAdapter(presets, selectedItemPosition, colorShape) {
|
||||||
when(it){
|
when (it) {
|
||||||
color -> {
|
color -> {
|
||||||
colorPickerDialogListener?.onColorSelected(dialogId, color)
|
colorPickerDialogListener?.onColorSelected(dialogId, color)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
else ->{
|
else -> {
|
||||||
color = it
|
color = it
|
||||||
if (showColorShades) {
|
if (showColorShades) {
|
||||||
createColorShades(color)
|
createColorShades(color)
|
||||||
|
@ -519,18 +517,18 @@ class ColorPickerDialog :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shadeColor(@ColorInt color: Int, percent: Double): Int {
|
private fun shadeColor(@ColorInt color: Int, percent: Double): Int {
|
||||||
val hex = String.format("#%06X", 0xFFFFFF and color)
|
val hex = "#%06X".format(color and 0xFFFFFF)
|
||||||
val f = hex.substring(1).toLong(16)
|
val f = hex.substring(1).toLong(16)
|
||||||
val t = (if (percent < 0) 0 else 255).toDouble()
|
val t = (if (percent < 0) 0 else 255).toDouble()
|
||||||
val p = if (percent < 0) percent * -1 else percent
|
val p = if (percent < 0) percent * -1 else percent
|
||||||
val R = f shr 16
|
val cR = f shr 16
|
||||||
val G = f shr 8 and 0x00FF
|
val cG = f shr 8 and 0x00FF
|
||||||
val B = f and 0x0000FF
|
val cB = f and 0x0000FF
|
||||||
return Color.argb(
|
return Color.argb(
|
||||||
Color.alpha(color),
|
Color.alpha(color),
|
||||||
((t - R) * p).roundToInt() + R.toInt(),
|
((t - cR) * p).roundToInt() + cR.toInt(),
|
||||||
((t - G) * p).roundToInt() + G.toInt(),
|
((t - cG) * p).roundToInt() + cG.toInt(),
|
||||||
((t - B) * p).roundToInt() + B.toInt(),
|
((t - cB) * p).roundToInt() + cB.toInt(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -502,13 +502,13 @@ class ColorPickerView @JvmOverloads constructor(
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun satValToPoint(sat: Float, `val`: Float): Point {
|
private fun satValToPoint(sat: Float, inValue: Float): Point {
|
||||||
val rect = satValRect
|
val rect = satValRect!!
|
||||||
val height = rect!!.height().toFloat()
|
val height = rect.height().toFloat()
|
||||||
val width = rect.width().toFloat()
|
val width = rect.width().toFloat()
|
||||||
val p = Point()
|
val p = Point()
|
||||||
p.x = (sat * width + rect.left).toInt()
|
p.x = (sat * width + rect.left).toInt()
|
||||||
p.y = ((1f - `val`) * height + rect.top).toInt()
|
p.y = ((1f - inValue) * height + rect.top).toInt()
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -78,11 +78,11 @@ class ActList : AppCompatActivity(), CoroutineScope {
|
||||||
grantResults: IntArray,
|
grantResults: IntArray,
|
||||||
) {
|
) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
if (requestCode == PERMISSION_REQUEST_CODE_STORAGE) {
|
// if (requestCode == PERMISSION_REQUEST_CODE_STORAGE) {
|
||||||
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
|
// if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
|
||||||
// 特に何もしてないらしい
|
// // 特に何もしてないらしい
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun load() = launch {
|
private fun load() = launch {
|
||||||
|
@ -153,14 +153,14 @@ class ActList : AppCompatActivity(), CoroutineScope {
|
||||||
|
|
||||||
inner class MyViewHolder(
|
inner class MyViewHolder(
|
||||||
viewRoot: View,
|
viewRoot: View,
|
||||||
_activity: ActList,
|
actList: ActList,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val tvCaption: TextView = viewRoot.findViewById(R.id.tvCaption)
|
private val tvCaption: TextView = viewRoot.findViewById(R.id.tvCaption)
|
||||||
private val apngView: ApngView = viewRoot.findViewById(R.id.apngView)
|
private val apngView: ApngView = viewRoot.findViewById(R.id.apngView)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
apngView.timeAnimationStart = _activity.timeAnimationStart
|
apngView.timeAnimationStart = actList.timeAnimationStart
|
||||||
}
|
}
|
||||||
|
|
||||||
private var lastId: Int = 0
|
private var lastId: Int = 0
|
||||||
|
|
|
@ -125,7 +125,7 @@ class ActViewer : AsyncActivity() {
|
||||||
Log.d(TAG, "$title[$i] timeWidth=${f.timeWidth}")
|
Log.d(TAG, "$title[$i] timeWidth=${f.timeWidth}")
|
||||||
val bitmap = f.bitmap
|
val bitmap = f.bitmap
|
||||||
|
|
||||||
FileOutputStream(File(dir, "${title}_${i}.png")).use { fo ->
|
FileOutputStream(File(dir, "${title}_$i.png")).use { fo ->
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fo)
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,119 +10,117 @@ import android.view.View
|
||||||
import jp.juggler.apng.ApngFrames
|
import jp.juggler.apng.ApngFrames
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class ApngView : View{
|
class ApngView : View {
|
||||||
|
|
||||||
var timeAnimationStart :Long =0L
|
|
||||||
|
|
||||||
var apngFrames : ApngFrames? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
initializeScale()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context : Context) : super(context, null) {
|
|
||||||
init(context)
|
|
||||||
}
|
|
||||||
constructor(context : Context, attrs : AttributeSet?) : super(context, attrs, 0) {
|
|
||||||
init(context)
|
|
||||||
}
|
|
||||||
constructor(context : Context, attrs : AttributeSet?, defStyle : Int) : super(context, attrs, defStyle) {
|
|
||||||
init(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var wView : Float = 1f
|
|
||||||
|
|
||||||
private var hView : Float = 1f
|
|
||||||
|
|
||||||
private var aspectView: Float = 1f
|
|
||||||
|
|
||||||
private var wImage : Float = 1f
|
|
||||||
|
|
||||||
private var hImage : Float = 1f
|
|
||||||
|
|
||||||
private var aspectImage : Float = 1f
|
|
||||||
|
|
||||||
|
|
||||||
|
var timeAnimationStart: Long = 0L
|
||||||
private var currentScale : Float = 1f
|
|
||||||
|
|
||||||
private var currentTransX :Float = 0f
|
|
||||||
private var currentTransY :Float = 0f
|
|
||||||
|
|
||||||
|
|
||||||
private val drawMatrix = Matrix()
|
|
||||||
|
|
||||||
private val paint = Paint()
|
|
||||||
|
|
||||||
private val findFrameResult = ApngFrames.FindFrameResult()
|
|
||||||
|
|
||||||
|
|
||||||
|
var apngFrames: ApngFrames? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
initializeScale()
|
||||||
|
}
|
||||||
|
|
||||||
private fun init(@Suppress("UNUSED_PARAMETER") context:Context){
|
constructor(context: Context) : super(context, null) {
|
||||||
//
|
init(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSizeChanged(w : Int, h : Int, oldw : Int, oldh : Int) {
|
|
||||||
super.onSizeChanged(w, h, oldw, oldh)
|
|
||||||
|
|
||||||
wView = max(1, w).toFloat()
|
|
||||||
hView = max(1, h).toFloat()
|
|
||||||
aspectView = wView / hView
|
|
||||||
|
|
||||||
initializeScale()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initializeScale(){
|
|
||||||
val apngFrames = this.apngFrames
|
|
||||||
if( apngFrames != null) {
|
|
||||||
wImage = max(1, apngFrames.width).toFloat()
|
|
||||||
hImage = max(1, apngFrames.height).toFloat()
|
|
||||||
aspectImage = wImage / hImage
|
|
||||||
|
|
||||||
currentScale = if(aspectView > aspectImage) {
|
|
||||||
hView / hImage
|
|
||||||
} else {
|
|
||||||
wView / wImage
|
|
||||||
}
|
|
||||||
|
|
||||||
val wDraw = wImage * currentScale
|
|
||||||
val hDraw = hImage * currentScale
|
|
||||||
|
|
||||||
currentTransX = (wView - wDraw) / 2f
|
|
||||||
currentTransY = (hView - hDraw) / 2f
|
|
||||||
|
|
||||||
} else {
|
|
||||||
currentScale = 1f
|
|
||||||
currentTransX = 0f
|
|
||||||
currentTransY = 0f
|
|
||||||
}
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onDraw(canvas : Canvas) {
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, 0) {
|
||||||
super.onDraw(canvas)
|
init(context)
|
||||||
|
}
|
||||||
val apngFrames = this.apngFrames
|
|
||||||
if(apngFrames != null ){
|
|
||||||
|
|
||||||
val t = SystemClock.elapsedRealtime() - timeAnimationStart
|
|
||||||
|
|
||||||
apngFrames.findFrame(findFrameResult,t)
|
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
|
||||||
val delay = findFrameResult.delay
|
context,
|
||||||
val bitmap = findFrameResult.bitmap
|
attrs,
|
||||||
if( bitmap != null) {
|
defStyle
|
||||||
drawMatrix.reset()
|
) {
|
||||||
drawMatrix.postScale(currentScale, currentScale)
|
init(context)
|
||||||
drawMatrix.postTranslate(currentTransX, currentTransY)
|
}
|
||||||
paint.isFilterBitmap = currentScale < 4f
|
|
||||||
canvas.drawBitmap(bitmap, drawMatrix, paint)
|
private var wView: Float = 1f
|
||||||
|
|
||||||
if( delay != Long.MAX_VALUE){
|
private var hView: Float = 1f
|
||||||
postInvalidateDelayed(max(1L,delay))
|
|
||||||
}
|
private var aspectView: Float = 1f
|
||||||
}
|
|
||||||
}
|
private var wImage: Float = 1f
|
||||||
}
|
|
||||||
|
private var hImage: Float = 1f
|
||||||
|
|
||||||
|
private var aspectImage: Float = 1f
|
||||||
|
|
||||||
|
private var currentScale: Float = 1f
|
||||||
|
|
||||||
|
private var currentTransX: Float = 0f
|
||||||
|
private var currentTransY: Float = 0f
|
||||||
|
|
||||||
|
private val drawMatrix = Matrix()
|
||||||
|
|
||||||
|
private val paint = Paint()
|
||||||
|
|
||||||
|
private val findFrameResult = ApngFrames.FindFrameResult()
|
||||||
|
|
||||||
|
private fun init(@Suppress("UNUSED_PARAMETER") context: Context) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
|
|
||||||
|
wView = max(1, w).toFloat()
|
||||||
|
hView = max(1, h).toFloat()
|
||||||
|
aspectView = wView / hView
|
||||||
|
|
||||||
|
initializeScale()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeScale() {
|
||||||
|
val apngFrames = this.apngFrames
|
||||||
|
if (apngFrames != null) {
|
||||||
|
wImage = max(1, apngFrames.width).toFloat()
|
||||||
|
hImage = max(1, apngFrames.height).toFloat()
|
||||||
|
aspectImage = wImage / hImage
|
||||||
|
|
||||||
|
currentScale = if (aspectView > aspectImage) {
|
||||||
|
hView / hImage
|
||||||
|
} else {
|
||||||
|
wView / wImage
|
||||||
|
}
|
||||||
|
|
||||||
|
val wDraw = wImage * currentScale
|
||||||
|
val hDraw = hImage * currentScale
|
||||||
|
|
||||||
|
currentTransX = (wView - wDraw) / 2f
|
||||||
|
currentTransY = (hView - hDraw) / 2f
|
||||||
|
} else {
|
||||||
|
currentScale = 1f
|
||||||
|
currentTransX = 0f
|
||||||
|
currentTransY = 0f
|
||||||
|
}
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
|
||||||
|
val apngFrames = this.apngFrames
|
||||||
|
if (apngFrames != null) {
|
||||||
|
|
||||||
|
val t = SystemClock.elapsedRealtime() - timeAnimationStart
|
||||||
|
|
||||||
|
apngFrames.findFrame(findFrameResult, t)
|
||||||
|
val delay = findFrameResult.delay
|
||||||
|
val bitmap = findFrameResult.bitmap
|
||||||
|
if (bitmap != null) {
|
||||||
|
drawMatrix.reset()
|
||||||
|
drawMatrix.postScale(currentScale, currentScale)
|
||||||
|
drawMatrix.postTranslate(currentTransX, currentTransY)
|
||||||
|
paint.isFilterBitmap = currentScale < 4f
|
||||||
|
canvas.drawBitmap(bitmap, drawMatrix, paint)
|
||||||
|
|
||||||
|
if (delay != Long.MAX_VALUE) {
|
||||||
|
postInvalidateDelayed(max(1L, delay))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue