runTestを使う。detektのチェック範囲を全モジュールとテストコードを含める。
This commit is contained in:
parent
b2b47e730a
commit
30b5beab64
|
@ -133,3 +133,4 @@ detektReport/
|
|||
|
||||
|
||||
app/detekt-*.xml
|
||||
.idea/androidTestResultsUserPreferences.xml
|
||||
|
|
|
@ -13,7 +13,7 @@ class ApngAnimationControl(
|
|||
|
||||
// if it is 0, the animation should play indefinitely.
|
||||
// If nonzero, the animation should come to rest on the final frame at the end of the last play.
|
||||
val numPlays : Int = PLAY_INDEFINITELY
|
||||
val numPlays: Int = PLAY_INDEFINITELY,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
@ -26,7 +26,6 @@ class ApngAnimationControl(
|
|||
numFrames = numFrames,
|
||||
numPlays = numPlays
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ internal class ApngChunk(crc32 : CRC32, tokenizer : StreamTokenizer) {
|
|||
val typeBytes = tokenizer.readBytes(4)
|
||||
type = typeBytes.toString(Charsets.UTF_8)
|
||||
|
||||
crc32.update(typeBytes)
|
||||
crc32.update(typeBytes, 0, typeBytes.size)
|
||||
}
|
||||
|
||||
fun readBody(crc32: CRC32, tokenizer: StreamTokenizer): ByteArray {
|
||||
|
@ -31,7 +31,6 @@ internal class ApngChunk(crc32 : CRC32, tokenizer : StreamTokenizer) {
|
|||
fun skipBody(tokenizer: StreamTokenizer) =
|
||||
tokenizer.skipBytes((size + 4).toLong())
|
||||
|
||||
|
||||
fun checkCRC(tokenizer: StreamTokenizer, crcActual: Long) {
|
||||
val crcExpect = tokenizer.readUInt32()
|
||||
if (crcActual != crcExpect) throw ApngParseError("CRC not match.")
|
||||
|
|
|
@ -12,7 +12,7 @@ object ApngDecoder {
|
|||
|
||||
fun parseStream(
|
||||
inStream: InputStream,
|
||||
callback : ApngDecoderCallback
|
||||
callback: ApngDecoderCallback,
|
||||
) {
|
||||
val apng = Apng()
|
||||
val tokenizer = StreamTokenizer(inStream)
|
||||
|
@ -153,7 +153,7 @@ object ApngDecoder {
|
|||
"tIME", // timestamp
|
||||
"hIST", // histogram
|
||||
"pHYs", // Physical pixel dimensions
|
||||
"sPLT" // Suggested palette (おそらく減色用?)
|
||||
"sPLT", // Suggested palette (おそらく減色用?)
|
||||
-> chunk.skipBody(tokenizer)
|
||||
|
||||
else -> {
|
||||
|
|
|
@ -17,7 +17,7 @@ interface ApngDecoderCallback {
|
|||
fun onAnimationInfo(
|
||||
apng: Apng,
|
||||
header: ApngImageHeader,
|
||||
animationControl : ApngAnimationControl
|
||||
animationControl: ApngAnimationControl,
|
||||
)
|
||||
|
||||
// called when default image bitmap was rendered.
|
||||
|
@ -27,5 +27,4 @@ interface ApngDecoderCallback {
|
|||
// its bitmap may same to default image for first frame.
|
||||
// ( in this case, both of onDefaultImage and onAnimationFrame are called for same bitmap)
|
||||
fun onAnimationFrame(apng: Apng, frameControl: ApngFrameControl, frameBitmap: ApngBitmap)
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ class ApngFrameControl (
|
|||
val disposeOp: DisposeOp,
|
||||
val blendOp: BlendOp,
|
||||
val sequenceNumber: Int,
|
||||
val delayMilliseconds: Long
|
||||
val delayMilliseconds: Long,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
@ -50,5 +50,4 @@ class ApngFrameControl (
|
|||
|
||||
override fun toString() =
|
||||
"ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayMilliseconds=$delayMilliseconds,disposeOp=$disposeOp,blendOp=$blendOp)"
|
||||
|
||||
}
|
|
@ -12,11 +12,10 @@ class ApngImageHeader(
|
|||
val colorType: ColorType,
|
||||
val compressionMethod: CompressionMethod,
|
||||
val filterMethod: FilterMethod,
|
||||
val interlaceMethod : InterlaceMethod
|
||||
val interlaceMethod: InterlaceMethod,
|
||||
) {
|
||||
companion object {
|
||||
internal fun parse(src: ByteSequence): ApngImageHeader {
|
||||
|
||||
val width = src.readInt32()
|
||||
val height = src.readInt32()
|
||||
if (width <= 0 || height <= 0) throw ApngParseError("w=$width,h=$height is too small")
|
||||
|
@ -44,8 +43,7 @@ class ApngImageHeader(
|
|||
colorType = colorType,
|
||||
compressionMethod = compressionMethod,
|
||||
filterMethod = filterMethod,
|
||||
interlaceMethod = interlaceMethod
|
||||
|
||||
interlaceMethod = interlaceMethod,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import jp.juggler.apng.util.getUInt8
|
|||
import kotlin.math.min
|
||||
|
||||
class ApngPalette(
|
||||
src : ByteArray // repeat of R,G,B
|
||||
src: ByteArray, // repeat of R,G,B
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package jp.juggler.apng
|
||||
|
||||
import java.io.InputStream
|
||||
import java.lang.StringBuilder
|
||||
import kotlin.math.min
|
||||
|
||||
// https://raw.githubusercontent.com/rtyley/animated-gif-lib-for-java/master/src/main/java/com/madgag/gif/fmsware/GifDecoder.java
|
||||
|
@ -32,7 +31,7 @@ class GifDecoder(val callback : GifDecoderCallback) {
|
|||
fun byte(): Int = bis.read()
|
||||
|
||||
// Reads next 16-bit value, LSB first
|
||||
fun UInt16() = byte() or (byte() shl 8)
|
||||
fun uInt16() = byte() or (byte() shl 8)
|
||||
|
||||
fun array(ba: ByteArray, offset: Int = 0, length: Int = ba.size - offset) {
|
||||
var nRead = 0
|
||||
|
@ -170,29 +169,27 @@ class GifDecoder(val callback : GifDecoderCallback) {
|
|||
// copy each source line to the appropriate place in the destination
|
||||
var pass = 1
|
||||
var inc = 8
|
||||
var iline = 0
|
||||
var iLine = 0
|
||||
for (i in 0 until srcRect.h) {
|
||||
var line = i
|
||||
if (interlace) {
|
||||
if(iline >= srcRect.h) {
|
||||
if (iLine >= srcRect.h) {
|
||||
when (++pass) {
|
||||
2 -> {
|
||||
iline = 4
|
||||
iLine = 4
|
||||
}
|
||||
|
||||
3 -> {
|
||||
iline = 2
|
||||
iLine = 2
|
||||
inc = 4
|
||||
}
|
||||
|
||||
4 -> {
|
||||
iline = 1
|
||||
iLine = 1
|
||||
inc = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
line = iline
|
||||
iline += inc
|
||||
line = iLine
|
||||
iLine += inc
|
||||
}
|
||||
line += srcRect.y
|
||||
if (line < height) {
|
||||
|
@ -212,7 +209,6 @@ class GifDecoder(val callback : GifDecoderCallback) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -234,14 +230,14 @@ class GifDecoder(val callback : GifDecoderCallback) {
|
|||
val pixelStack = this.pixelStack!!
|
||||
|
||||
// Initialize GIF data stream decoder.
|
||||
val data_size = reader.byte()
|
||||
val clear = 1 shl data_size
|
||||
val end_of_information = clear + 1
|
||||
val dataSize = reader.byte()
|
||||
val clear = 1 shl dataSize
|
||||
val endOfInformation = clear + 1
|
||||
|
||||
var available = clear + 2
|
||||
var old_code = NullCode
|
||||
var code_size = data_size + 1
|
||||
var code_mask = (1 shl code_size) - 1
|
||||
var oldCode = NullCode
|
||||
var codeSize = dataSize + 1
|
||||
var codeMask = (1 shl codeSize) - 1
|
||||
|
||||
for (code in 0 until clear) {
|
||||
prefix[code] = 0
|
||||
|
@ -260,7 +256,7 @@ class GifDecoder(val callback : GifDecoderCallback) {
|
|||
var i = 0
|
||||
while (i < nPixels) {
|
||||
if (top == 0) {
|
||||
if(bits < code_size) {
|
||||
if (bits < codeSize) {
|
||||
// Load bytes until there are enough bits for a code.
|
||||
if (count == 0) {
|
||||
// Read a new data block.
|
||||
|
@ -277,32 +273,32 @@ class GifDecoder(val callback : GifDecoderCallback) {
|
|||
}
|
||||
|
||||
// Get the next code.
|
||||
var code = datum and code_mask
|
||||
datum = datum shr code_size
|
||||
bits -= code_size
|
||||
var code = datum and codeMask
|
||||
datum = datum shr codeSize
|
||||
bits -= codeSize
|
||||
|
||||
// Interpret the code
|
||||
if((code > available) || (code == end_of_information)) break
|
||||
if ((code > available) || (code == endOfInformation)) break
|
||||
|
||||
if (code == clear) {
|
||||
// Reset decoder.
|
||||
code_size = data_size + 1
|
||||
code_mask = (1 shl code_size) - 1
|
||||
codeSize = dataSize + 1
|
||||
codeMask = (1 shl codeSize) - 1
|
||||
available = clear + 2
|
||||
old_code = NullCode
|
||||
oldCode = NullCode
|
||||
continue
|
||||
}
|
||||
if(old_code == NullCode) {
|
||||
if (oldCode == NullCode) {
|
||||
pixelStack[top++] = suffix[code]
|
||||
old_code = code
|
||||
oldCode = code
|
||||
first = code
|
||||
continue
|
||||
}
|
||||
val in_code = code
|
||||
val inCode = code
|
||||
|
||||
if (code == available) {
|
||||
pixelStack[top++] = first.toByte()
|
||||
code = old_code
|
||||
code = oldCode
|
||||
}
|
||||
while (code > clear) {
|
||||
pixelStack[top++] = suffix[code]
|
||||
|
@ -317,16 +313,16 @@ class GifDecoder(val callback : GifDecoderCallback) {
|
|||
continue
|
||||
}
|
||||
pixelStack[top++] = first.toByte()
|
||||
prefix[available] = old_code.toShort()
|
||||
prefix[available] = oldCode.toShort()
|
||||
suffix[available] = first.toByte()
|
||||
available++
|
||||
|
||||
if((available and code_mask) == 0 && available < MaxStackSize) {
|
||||
code_size ++
|
||||
code_mask += available
|
||||
if ((available and codeMask) == 0 && available < MaxStackSize) {
|
||||
codeSize++
|
||||
codeMask += available
|
||||
}
|
||||
|
||||
old_code = in_code
|
||||
oldCode = inCode
|
||||
}
|
||||
|
||||
// Pop a pixel off the pixel stack.
|
||||
|
@ -382,7 +378,7 @@ class GifDecoder(val callback : GifDecoderCallback) {
|
|||
|
||||
transparency = (packed and 1) != 0
|
||||
// delay in milliseconds
|
||||
delay = reader.UInt16() * 10
|
||||
delay = reader.uInt16() * 10
|
||||
// transparent color index
|
||||
transIndex = reader.byte()
|
||||
// block terminator
|
||||
|
@ -405,10 +401,10 @@ class GifDecoder(val callback : GifDecoderCallback) {
|
|||
// Reads next frame image
|
||||
private fun parseFrame(reader: Reader) {
|
||||
// (sub)image position & size
|
||||
srcRect.x = reader.UInt16()
|
||||
srcRect.y = reader.UInt16()
|
||||
srcRect.w = reader.UInt16()
|
||||
srcRect.h = reader.UInt16()
|
||||
srcRect.x = reader.uInt16()
|
||||
srcRect.y = reader.uInt16()
|
||||
srcRect.w = reader.uInt16()
|
||||
srcRect.h = reader.uInt16()
|
||||
|
||||
val packed = reader.byte()
|
||||
lctFlag = (packed and 0x80) != 0 // 1 - local color table flag
|
||||
|
@ -540,9 +536,9 @@ class GifDecoder(val callback : GifDecoderCallback) {
|
|||
*/
|
||||
|
||||
// logical screen size
|
||||
width = reader.UInt16()
|
||||
height = reader.UInt16()
|
||||
if(width < 1 || height < 1) error("too small size. ${width}*${height}")
|
||||
width = reader.uInt16()
|
||||
height = reader.uInt16()
|
||||
if (width < 1 || height < 1) error("too small size. $width*$height")
|
||||
|
||||
// packed fields
|
||||
val packed = reader.byte()
|
||||
|
|
|
@ -7,5 +7,4 @@ interface GifDecoderCallback {
|
|||
fun onGifHeader(header: ApngImageHeader)
|
||||
fun onGifAnimationInfo(header: ApngImageHeader, animationControl: ApngAnimationControl)
|
||||
fun onGifAnimationFrame(frameControl: ApngFrameControl, frameBitmap: ApngBitmap)
|
||||
|
||||
}
|
|
@ -14,7 +14,7 @@ internal class IdatDecoder(
|
|||
private val bitmap: ApngBitmap,
|
||||
private val inflateBufferPool: BufferPool,
|
||||
private val callback: ApngDecoderCallback,
|
||||
private val onCompleted: () -> Unit
|
||||
private val onCompleted: () -> Unit,
|
||||
) {
|
||||
|
||||
private class PassInfo(val xStep: Int, val xStart: Int, val yStep: Int, val yStart: Int)
|
||||
|
@ -47,9 +47,9 @@ internal class IdatDecoder(
|
|||
}
|
||||
}
|
||||
|
||||
private inline fun scanLine1(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
||||
private inline fun scanLine1(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var remain = pass_w
|
||||
var remain = passW
|
||||
while (remain >= 8) {
|
||||
remain -= 8
|
||||
val v = baLine[pos++].toInt()
|
||||
|
@ -74,9 +74,9 @@ internal class IdatDecoder(
|
|||
}
|
||||
}
|
||||
|
||||
private inline fun scanLine2(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
||||
private inline fun scanLine2(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var remain = pass_w
|
||||
var remain = passW
|
||||
while (remain >= 4) {
|
||||
remain -= 4
|
||||
val v = baLine[pos++].toInt()
|
||||
|
@ -93,9 +93,9 @@ internal class IdatDecoder(
|
|||
}
|
||||
}
|
||||
|
||||
private inline fun scanLine4(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
||||
private inline fun scanLine4(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var remain = pass_w
|
||||
var remain = passW
|
||||
while (remain >= 2) {
|
||||
remain -= 2
|
||||
val v = baLine[pos++].toInt()
|
||||
|
@ -108,14 +108,13 @@ internal class IdatDecoder(
|
|||
}
|
||||
}
|
||||
|
||||
private inline fun scanLine8(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
||||
private inline fun scanLine8(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var remain = pass_w
|
||||
var remain = passW
|
||||
while (remain-- > 0) {
|
||||
block(baLine.getUInt8(pos++))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val inflater = Inflater()
|
||||
|
@ -424,7 +423,6 @@ internal class IdatDecoder(
|
|||
// val y = passInfo.yStart + passInfo.yStep * passY
|
||||
// callback.onApngDebug("sub pos=$pos,x=$x,y=$y,left=$vLeft,cur=$vCur,after=${baLine[pos].toInt() and 255}")
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -464,7 +462,6 @@ internal class IdatDecoder(
|
|||
// val y = passInfo.yStart + passInfo.yStep * passY
|
||||
// callback.onApngDebug("paeth pos=$pos,x=$x,y=$y,left=$vLeft,up=$vUp,ul=$vUpperLeft,cur=$vCur,paeth=${paeth(vLeft, vUp, vUpperLeft)}")
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -498,7 +495,7 @@ internal class IdatDecoder(
|
|||
inStream: InputStream,
|
||||
size: Int,
|
||||
inBuffer: ByteArray,
|
||||
crc32: CRC32
|
||||
crc32: CRC32,
|
||||
) {
|
||||
var foundEnd = false
|
||||
var inRemain = size
|
||||
|
@ -541,7 +538,7 @@ internal class IdatDecoder(
|
|||
inflateBufferQueue.add(ByteSequence(buffer, 0, nInflated))
|
||||
|
||||
// キューに追加したデータをScanLine単位で消費する
|
||||
@Suppress("ControlFlowWithEmptyBody")
|
||||
@Suppress("ControlFlowWithEmptyBody", "EmptyWhileBlock")
|
||||
while (!isCompleted && readScanLine()) {
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ internal fun ByteArray.getInt32(pos : Int) = (getUInt8(pos) shl 24) or
|
|||
internal class ByteSequence(
|
||||
val array: ByteArray,
|
||||
var offset: Int,
|
||||
var length : Int
|
||||
var length: Int,
|
||||
) {
|
||||
|
||||
constructor(ba: ByteArray) : this(ba, 0, ba.size)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@file:Suppress("LocalVariableName")
|
||||
|
||||
package jp.juggler.apng
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
@ -7,6 +8,7 @@ import java.io.BufferedInputStream
|
|||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class TestApng {
|
||||
|
||||
companion object {
|
||||
|
@ -3124,7 +3126,7 @@ class TestApng {
|
|||
override fun onAnimationInfo(
|
||||
apng: Apng,
|
||||
header: ApngImageHeader,
|
||||
animationControl : ApngAnimationControl
|
||||
animationControl: ApngAnimationControl,
|
||||
) {
|
||||
println("animationControl=$animationControl")
|
||||
}
|
||||
|
@ -3140,7 +3142,7 @@ class TestApng {
|
|||
override fun onAnimationFrame(
|
||||
apng: Apng,
|
||||
frameControl: ApngFrameControl,
|
||||
bitmap : ApngBitmap
|
||||
bitmap: ApngBitmap,
|
||||
) {
|
||||
println("onAnimationFrame frameControl=$frameControl")
|
||||
println("onAnimationFrame w=${bitmap.width},h=${bitmap.height}")
|
||||
|
@ -3172,7 +3174,7 @@ class TestApng {
|
|||
override fun onAnimationInfo(
|
||||
apng: Apng,
|
||||
header: ApngImageHeader,
|
||||
animationControl : ApngAnimationControl
|
||||
animationControl: ApngAnimationControl,
|
||||
) {
|
||||
println("animationControl=$animationControl")
|
||||
}
|
||||
|
@ -3219,7 +3221,7 @@ class TestApng {
|
|||
override fun onAnimationFrame(
|
||||
apng: Apng,
|
||||
frameControl: ApngFrameControl,
|
||||
bitmap : ApngBitmap
|
||||
bitmap: ApngBitmap,
|
||||
) {
|
||||
println("onAnimationFrame frameControl=$frameControl")
|
||||
println("onAnimationFrame w=${bitmap.width},h=${bitmap.height}")
|
||||
|
|
|
@ -1,21 +1,14 @@
|
|||
package jp.juggler.apng
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffXfermode
|
||||
import android.graphics.Rect
|
||||
import android.graphics.*
|
||||
import android.util.Log
|
||||
|
||||
import java.io.InputStream
|
||||
import java.util.ArrayList
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class ApngFrames private constructor(
|
||||
private val pixelSizeMax: Int = 0,
|
||||
private val debug : Boolean = false
|
||||
private val debug: Boolean = false,
|
||||
) : ApngDecoderCallback, GifDecoderCallback {
|
||||
|
||||
companion object {
|
||||
|
@ -42,14 +35,14 @@ class ApngFrames private constructor(
|
|||
(max.toFloat() * num.toFloat() / den.toFloat() + 0.5f).toInt()
|
||||
|
||||
private fun scaleBitmap(
|
||||
size_max : Int,
|
||||
sizeMax: Int,
|
||||
src: Bitmap,
|
||||
recycleSrc : Boolean = true // true: ownership of "src" will be moved or recycled.
|
||||
recycleSrc: Boolean = true, // true: ownership of "src" will be moved or recycled.
|
||||
): Bitmap {
|
||||
|
||||
val wSrc = src.width
|
||||
val hSrc = src.height
|
||||
if(size_max <= 0 || (wSrc <= size_max && hSrc <= size_max)) {
|
||||
if (sizeMax <= 0 || (wSrc <= sizeMax && hSrc <= sizeMax)) {
|
||||
return if (recycleSrc) {
|
||||
src
|
||||
} else {
|
||||
|
@ -60,11 +53,11 @@ class ApngFrames private constructor(
|
|||
val wDst: Int
|
||||
val hDst: Int
|
||||
if (wSrc >= hSrc) {
|
||||
wDst = size_max
|
||||
hDst = max(1, scale(size_max, hSrc, wSrc))
|
||||
wDst = sizeMax
|
||||
hDst = max(1, scale(sizeMax, hSrc, wSrc))
|
||||
} else {
|
||||
hDst = size_max
|
||||
wDst = max(1, scale(size_max, wSrc, hSrc))
|
||||
hDst = sizeMax
|
||||
wDst = max(1, scale(sizeMax, wSrc, hSrc))
|
||||
}
|
||||
//Log.v(TAG,"scaleBitmap: $wSrc,$hSrc => $wDst,$hDst")
|
||||
|
||||
|
@ -89,38 +82,37 @@ class ApngFrames private constructor(
|
|||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
|
||||
private fun toAndroidBitmap(src : ApngBitmap, size_max : Int) =
|
||||
scaleBitmap(size_max, toAndroidBitmap(src))
|
||||
private fun toAndroidBitmap(src: ApngBitmap, sizeMax: Int) =
|
||||
scaleBitmap(sizeMax, toAndroidBitmap(src))
|
||||
|
||||
private fun parseApng(
|
||||
inStream: InputStream,
|
||||
pixelSizeMax: Int,
|
||||
debug : Boolean = false
|
||||
debug: Boolean = false,
|
||||
): ApngFrames {
|
||||
val result = ApngFrames(pixelSizeMax, debug)
|
||||
try {
|
||||
ApngDecoder.parseStream(inStream, result)
|
||||
result.onParseComplete()
|
||||
return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
|
||||
?: throw RuntimeException("APNG has no image")
|
||||
?: error("APNG has no image")
|
||||
} catch (ex: Throwable) {
|
||||
result.dispose()
|
||||
throw ex
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun parseGif(
|
||||
inStream: InputStream,
|
||||
pixelSizeMax: Int,
|
||||
debug : Boolean = false
|
||||
debug: Boolean = false,
|
||||
): ApngFrames {
|
||||
val result = ApngFrames(pixelSizeMax, debug)
|
||||
try {
|
||||
GifDecoder(result).parse(inStream)
|
||||
result.onParseComplete()
|
||||
return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
|
||||
?: throw RuntimeException("GIF has no image")
|
||||
?: error("GIF has no image")
|
||||
} catch (ex: Throwable) {
|
||||
result.dispose()
|
||||
throw ex
|
||||
|
@ -130,7 +122,11 @@ class ApngFrames private constructor(
|
|||
private val apngHeadKey = byteArrayOf(0x89.toByte(), 0x50)
|
||||
private val gifHeadKey = "GIF".toByteArray(Charsets.UTF_8)
|
||||
|
||||
private fun matchBytes(ba1:ByteArray,ba2:ByteArray,length:Int=min(ba1.size,ba2.size)):Boolean{
|
||||
private fun matchBytes(
|
||||
ba1: ByteArray,
|
||||
ba2: ByteArray,
|
||||
length: Int = min(ba1.size, ba2.size),
|
||||
): Boolean {
|
||||
for (i in 0 until length) {
|
||||
if (ba1[i] != ba2[i]) return false
|
||||
}
|
||||
|
@ -140,7 +136,7 @@ class ApngFrames private constructor(
|
|||
fun parse(
|
||||
pixelSizeMax: Int,
|
||||
debug: Boolean = false,
|
||||
opener : () -> InputStream?
|
||||
opener: () -> InputStream?,
|
||||
): ApngFrames? {
|
||||
|
||||
val buf = ByteArray(8) { 0.toByte() }
|
||||
|
@ -171,7 +167,7 @@ class ApngFrames private constructor(
|
|||
class Frame(
|
||||
val bitmap: Bitmap,
|
||||
val timeStart: Long,
|
||||
val timeWidth : Long
|
||||
val timeWidth: Long,
|
||||
)
|
||||
|
||||
var frames: ArrayList<Frame>? = null
|
||||
|
@ -322,7 +318,7 @@ class ApngFrames private constructor(
|
|||
override fun onAnimationInfo(
|
||||
apng: Apng,
|
||||
header: ApngImageHeader,
|
||||
animationControl : ApngAnimationControl
|
||||
animationControl: ApngAnimationControl,
|
||||
) {
|
||||
if (debug) {
|
||||
Log.d(TAG, "onAnimationInfo")
|
||||
|
@ -346,7 +342,7 @@ class ApngFrames private constructor(
|
|||
override fun onAnimationFrame(
|
||||
apng: Apng,
|
||||
frameControl: ApngFrameControl,
|
||||
frameBitmap : ApngBitmap
|
||||
frameBitmap: ApngBitmap,
|
||||
) {
|
||||
if (debug) {
|
||||
Log.d(
|
||||
|
@ -435,9 +431,7 @@ class ApngFrames private constructor(
|
|||
sPaintDontBlend
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} finally {
|
||||
frameBitmapAndroid.recycle()
|
||||
}
|
||||
|
@ -465,7 +459,7 @@ class ApngFrames private constructor(
|
|||
|
||||
override fun onGifAnimationInfo(
|
||||
header: ApngImageHeader,
|
||||
animationControl : ApngAnimationControl
|
||||
animationControl: ApngAnimationControl,
|
||||
) {
|
||||
if (debug) {
|
||||
Log.d(TAG, "onAnimationInfo")
|
||||
|
@ -479,7 +473,7 @@ class ApngFrames private constructor(
|
|||
|
||||
override fun onGifAnimationFrame(
|
||||
frameControl: ApngFrameControl,
|
||||
frameBitmap : ApngBitmap
|
||||
frameBitmap: ApngBitmap,
|
||||
) {
|
||||
if (debug) {
|
||||
Log.d(
|
||||
|
@ -504,11 +498,8 @@ class ApngFrames private constructor(
|
|||
)
|
||||
frames.add(frame)
|
||||
timeTotal += frame.timeWidth
|
||||
|
||||
} finally {
|
||||
frameBitmapAndroid.recycle()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -135,6 +135,7 @@ dependencies {
|
|||
implementation project(':apng_android')
|
||||
implementation fileTree(include: ['*.aar'], dir: 'src/main/libs')
|
||||
|
||||
|
||||
// App1 とサーバ情報カラムで使う
|
||||
api "org.conscrypt:conscrypt-android:2.5.2"
|
||||
|
||||
|
@ -143,19 +144,48 @@ dependencies {
|
|||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
|
||||
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detekt_version")
|
||||
|
||||
testImplementation(project(":base"))
|
||||
androidTestImplementation(project(":base"))
|
||||
|
||||
testApi "androidx.arch.core:core-testing:$arch_version"
|
||||
testApi "junit:junit:$junit_version"
|
||||
testApi "org.jetbrains.kotlin:kotlin-test"
|
||||
testApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
|
||||
|
||||
androidTestApi "androidx.test.espresso:espresso-core:3.5.1"
|
||||
androidTestApi "androidx.test.ext:junit-ktx:1.1.5"
|
||||
androidTestApi "androidx.test.ext:junit:1.1.5"
|
||||
androidTestApi "androidx.test.ext:truth:1.5.0"
|
||||
androidTestApi "androidx.test:core-ktx:1.5.0"
|
||||
androidTestApi "androidx.test:core:$androidx_test_version"
|
||||
androidTestApi "androidx.test:runner:1.5.2"
|
||||
androidTestApi "org.jetbrains.kotlin:kotlin-test"
|
||||
androidTestApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
|
||||
|
||||
// To use android test orchestrator
|
||||
androidTestUtil "androidx.test:orchestrator:1.4.2"
|
||||
|
||||
testApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") {
|
||||
exclude group: "com.squareup.okio", module: "okio"
|
||||
exclude group: "com.squareup.okhttp3", module: "okhttp"
|
||||
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-common"
|
||||
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
|
||||
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jdk8"
|
||||
}
|
||||
androidTestApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") {
|
||||
exclude group: "com.squareup.okio", module: "okio"
|
||||
exclude group: "com.squareup.okhttp3", module: "okhttp"
|
||||
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-common"
|
||||
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
|
||||
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jdk8"
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
// detekt
|
||||
def projectSource = file(projectDir)
|
||||
def configFile = files("$rootDir/config/detekt/config.yml")
|
||||
def baselineFile = file("$rootDir/config/detekt/baseline.xml")
|
||||
def kotlinFiles = "**/*.kt"
|
||||
def resourceFiles = "**/resources/**"
|
||||
def buildFiles = "**/build/**"
|
||||
|
||||
tasks.register("detektAll", Detekt) {
|
||||
description = "Custom DETEKT build for all modules"
|
||||
|
@ -173,12 +203,30 @@ tasks.register("detektAll", Detekt) {
|
|||
// preconfigure defaults
|
||||
buildUponDefaultConfig = true
|
||||
|
||||
setSource(projectSource)
|
||||
def configFile = files("$rootDir/config/detekt/config.yml")
|
||||
config.setFrom(configFile)
|
||||
|
||||
def baselineFile = file("$rootDir/config/detekt/baseline.xml")
|
||||
if (baselineFile.isFile()) {
|
||||
baseline.set(baselineFile)
|
||||
}
|
||||
include(kotlinFiles)
|
||||
|
||||
source = files(
|
||||
"$rootDir/apng/src",
|
||||
"$rootDir/apng_android/src",
|
||||
"$rootDir/app/src",
|
||||
"$rootDir/base/src",
|
||||
"$rootDir/colorpicker/src",
|
||||
"$rootDir/emoji/src",
|
||||
"$rootDir/icon_material_symbols/src",
|
||||
"$rootDir/sample_apng/src",
|
||||
)
|
||||
|
||||
// def kotlinFiles = "**/*.kt"
|
||||
// include(kotlinFiles)
|
||||
|
||||
def resourceFiles = "**/resources/**"
|
||||
def buildFiles = "**/build/**"
|
||||
exclude(resourceFiles, buildFiles)
|
||||
reports {
|
||||
html.enabled = true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import jp.juggler.util.jsonArray
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import jp.juggler.util.data.*
|
||||
import org.jetbrains.anko.collections.forEachReversedByIndex
|
||||
import org.jetbrains.anko.collections.forEachReversedWithIndex
|
||||
import org.junit.Assert.assertEquals
|
||||
|
@ -19,7 +19,7 @@ class JsonArrayForEach {
|
|||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun test() {
|
||||
val array = jsonArray {
|
||||
val array = buildJsonArray {
|
||||
add("a")
|
||||
add("b")
|
||||
add(null)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.graphics.Color
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.jrummyapps.android.colorpicker.parseColorString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccount
|
||||
import jp.juggler.util.data.asciiPatternString
|
||||
import org.junit.Assert.assertEquals
|
||||
|
|
|
@ -2,7 +2,7 @@ package jp.juggler.subwaytooter
|
|||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.util.asciiRegex
|
||||
import jp.juggler.util.data.asciiRegex
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
@ -1,55 +1,48 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import jp.juggler.subwaytooter.api.TootApiCallback
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.entity.Host
|
||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.testutil.MainDispatcherRule
|
||||
import jp.juggler.subwaytooter.testutil.MockInterceptor
|
||||
import jp.juggler.subwaytooter.util.SimpleHttpClientImpl
|
||||
import jp.juggler.util.coroutine.AppDispatchers
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.network.MySslSocketFactory
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.ConnectionSpec
|
||||
import okhttp3.OkHttpClient
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.*
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TestTootInstance {
|
||||
|
||||
companion object {
|
||||
private val log = LogCategory("TestTootInstance")
|
||||
}
|
||||
|
||||
// val cookieJar = JavaNetCookieJar(CookieManager().apply {
|
||||
// setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
||||
// CookieHandler.setDefault(this)
|
||||
// })
|
||||
// テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
|
||||
// プロパティは記述順に初期化されることに注意
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val okHttp = OkHttpClient.Builder()
|
||||
.connectTimeout(60.toLong(), TimeUnit.SECONDS)
|
||||
.readTimeout(60.toLong(), TimeUnit.SECONDS)
|
||||
.writeTimeout(60.toLong(), TimeUnit.SECONDS)
|
||||
.pingInterval(10, TimeUnit.SECONDS)
|
||||
.connectionSpecs(
|
||||
Collections.singletonList(
|
||||
ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
||||
.allEnabledCipherSuites()
|
||||
.allEnabledTlsVersions()
|
||||
.build()
|
||||
private val client by lazy {
|
||||
val mockInterceptor = MockInterceptor(
|
||||
// テストアプリのコンテキスト
|
||||
context = InstrumentationRegistry.getInstrumentation().context!!,
|
||||
// テストアプリ中のリソースID
|
||||
rawId = jp.juggler.subwaytooter.test.R.raw.test_toot_instance_mock,
|
||||
)
|
||||
)
|
||||
.sslSocketFactory(MySslSocketFactory, MySslSocketFactory.trustManager)
|
||||
.build()
|
||||
|
||||
private val dummyClientCallback = object : TootApiCallback {
|
||||
val okHttp = OkHttpClient.Builder().addInterceptor(mockInterceptor).build()
|
||||
|
||||
val dummyClientCallback = object : TootApiCallback {
|
||||
override suspend fun isApiCancelled() = false
|
||||
|
||||
override suspend fun publishApiProgress(s: String) {
|
||||
|
@ -61,9 +54,8 @@ class TestTootInstance {
|
|||
}
|
||||
}
|
||||
|
||||
private val appContext = InstrumentationRegistry.getInstrumentation().targetContext!!
|
||||
|
||||
val client = TootApiClient(
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext!!
|
||||
TootApiClient(
|
||||
context = appContext,
|
||||
httpClient = SimpleHttpClientImpl(appContext, okHttp),
|
||||
callback = dummyClientCallback
|
||||
|
@ -76,25 +68,19 @@ class TestTootInstance {
|
|||
*/
|
||||
|
||||
@Test
|
||||
fun testWithoutAccount() {
|
||||
runBlocking {
|
||||
withContext(AppDispatchers.io) {
|
||||
fun instanceByHostname() = runTest {
|
||||
suspend fun a(host: Host) {
|
||||
val (ti, ri) = TootInstance.getEx(client, hostArg = host)
|
||||
assertNotNull(ti)
|
||||
assertNull(ri?.error)
|
||||
assertNull("no error", ri?.error)
|
||||
assertNotNull("instance information", ti)
|
||||
ti!!.run { log.d("$instanceType $uri $version") }
|
||||
}
|
||||
a(Host.parse("mastodon.juggler.jp"))
|
||||
a(Host.parse("misskey.io"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWithAccount() {
|
||||
runBlocking {
|
||||
withContext(AppDispatchers.io) {
|
||||
fun testWithAccount() = runTest {
|
||||
suspend fun a(account: SavedAccount) {
|
||||
val (ti, ri) = TootInstance.getEx(client, account = account)
|
||||
assertNull(ri?.error)
|
||||
|
@ -105,5 +91,3 @@ class TestTootInstance {
|
|||
a(SavedAccount(45, "tateisu@misskey.io", misskeyVersion = 12))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import jp.juggler.util.data.CharacterGroup
|
||||
import jp.juggler.util.data.WordTrieTree
|
||||
import org.junit.Assert.assertEquals
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package jp.juggler.subwaytooter.api
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.util.data.*
|
||||
|
|
|
@ -2,18 +2,19 @@
|
|||
|
||||
package jp.juggler.subwaytooter.api
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import jp.juggler.subwaytooter.api.entity.Host
|
||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.testutil.MainDispatcherRule
|
||||
import jp.juggler.subwaytooter.util.SimpleHttpClient
|
||||
import jp.juggler.util.data.JsonObject
|
||||
import jp.juggler.util.data.buildJsonArray
|
||||
import jp.juggler.util.data.buildJsonObject
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.network.MEDIA_TYPE_JSON
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.*
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
|
@ -21,6 +22,7 @@ import okio.Buffer
|
|||
import okio.BufferedSource
|
||||
import okio.ByteString
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
@ -28,6 +30,12 @@ import java.util.concurrent.atomic.AtomicReference
|
|||
@Suppress("MemberVisibilityCanPrivate")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TestTootApiClient {
|
||||
|
||||
// テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
|
||||
// プロパティは記述順に初期化されることに注意
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
companion object {
|
||||
private val log = LogCategory("TestTootApiClient")
|
||||
}
|
||||
|
@ -229,8 +237,15 @@ class TestTootApiClient {
|
|||
.build()
|
||||
}
|
||||
|
||||
else ->
|
||||
Response.Builder()
|
||||
"/api/meta" -> Response.Builder()
|
||||
.request(request)
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(404)
|
||||
.message("not found")
|
||||
.body("""{"error":"404 not found"}""".toResponseBody(MEDIA_TYPE_JSON))
|
||||
.build()
|
||||
|
||||
else -> Response.Builder()
|
||||
.request(request)
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(200)
|
||||
|
@ -519,7 +534,7 @@ class TestTootApiClient {
|
|||
|
||||
@Test
|
||||
fun testIsApiCancelled() {
|
||||
runBlocking {
|
||||
runTest {
|
||||
var flag = 0
|
||||
var progressString: String? = null
|
||||
var progressValue: Int? = null
|
||||
|
@ -559,7 +574,7 @@ class TestTootApiClient {
|
|||
|
||||
@Test
|
||||
fun testSendRequest() {
|
||||
runBlocking {
|
||||
runTest {
|
||||
|
||||
val callback = ProgressRecordTootApiCallback()
|
||||
|
||||
|
@ -637,7 +652,7 @@ class TestTootApiClient {
|
|||
|
||||
@Test
|
||||
fun testReadBodyString() {
|
||||
runBlocking {
|
||||
runTest {
|
||||
val callback = ProgressRecordTootApiCallback()
|
||||
val client = TootApiClient(
|
||||
appContext,
|
||||
|
@ -745,7 +760,7 @@ class TestTootApiClient {
|
|||
|
||||
@Test
|
||||
fun testParseString() {
|
||||
runBlocking {
|
||||
runTest {
|
||||
|
||||
val callback = ProgressRecordTootApiCallback()
|
||||
val client = TootApiClient(
|
||||
|
@ -865,7 +880,7 @@ class TestTootApiClient {
|
|||
|
||||
@Test
|
||||
fun testParseJson() {
|
||||
runBlocking {
|
||||
runTest {
|
||||
val callback = ProgressRecordTootApiCallback()
|
||||
val client = TootApiClient(
|
||||
appContext,
|
||||
|
@ -1046,7 +1061,7 @@ class TestTootApiClient {
|
|||
|
||||
@Test
|
||||
fun testRegisterClient() {
|
||||
runBlocking {
|
||||
runTest {
|
||||
val callback = ProgressRecordTootApiCallback()
|
||||
val client = TootApiClient(
|
||||
appContext,
|
||||
|
@ -1064,7 +1079,7 @@ class TestTootApiClient {
|
|||
assertEquals(null, result?.error)
|
||||
var jsonObject = result?.jsonObject
|
||||
assertNotNull(jsonObject)
|
||||
if (jsonObject == null) return@runBlocking
|
||||
if (jsonObject == null) return@runTest
|
||||
val clientInfo = jsonObject
|
||||
|
||||
// clientCredential の作成
|
||||
|
@ -1073,7 +1088,7 @@ class TestTootApiClient {
|
|||
assertEquals(null, result?.error)
|
||||
val clientCredential = result?.string
|
||||
assertNotNull(clientCredential)
|
||||
if (clientCredential == null) return@runBlocking
|
||||
if (clientCredential == null) return@runTest
|
||||
clientInfo[TootApiClient.KEY_CLIENT_CREDENTIAL] = clientCredential
|
||||
|
||||
// clientCredential の検証
|
||||
|
@ -1082,7 +1097,7 @@ class TestTootApiClient {
|
|||
assertEquals(null, result?.error)
|
||||
jsonObject = result?.jsonObject
|
||||
assertNotNull(jsonObject) // 中味は別に見てない。jsonObjectなら良いらしい
|
||||
if (jsonObject == null) return@runBlocking
|
||||
if (jsonObject == null) return@runTest
|
||||
|
||||
var url: String?
|
||||
|
||||
|
@ -1095,7 +1110,7 @@ class TestTootApiClient {
|
|||
result = client.authentication1(clientName)
|
||||
url = result?.string
|
||||
assertNotNull(url)
|
||||
if (url == null) return@runBlocking
|
||||
if (url == null) return@runTest
|
||||
println(url)
|
||||
|
||||
// ブラウザからコールバックで受け取ったcodeを処理する
|
||||
|
@ -1103,29 +1118,29 @@ class TestTootApiClient {
|
|||
result = client.authentication2Mastodon(clientName, "DUMMY_CODE", refToken)
|
||||
jsonObject = result?.jsonObject
|
||||
assertNotNull(jsonObject)
|
||||
if (jsonObject == null) return@runBlocking
|
||||
if (jsonObject == null) return@runTest
|
||||
println(jsonObject.toString())
|
||||
|
||||
// 認証できたならアクセストークンがある
|
||||
val tokenInfo = result?.tokenInfo
|
||||
assertNotNull(tokenInfo)
|
||||
if (tokenInfo == null) return@runBlocking
|
||||
if (tokenInfo == null) return@runTest
|
||||
val accessToken = tokenInfo.string("access_token")
|
||||
assertNotNull(accessToken)
|
||||
if (accessToken == null) return@runBlocking
|
||||
if (accessToken == null) return@runTest
|
||||
|
||||
// アカウント手動入力でログインする場合はこの関数を直接呼び出す
|
||||
result = client.getUserCredential(accessToken, tokenInfo)
|
||||
jsonObject = result?.jsonObject
|
||||
assertNotNull(jsonObject)
|
||||
if (jsonObject == null) return@runBlocking
|
||||
if (jsonObject == null) return@runTest
|
||||
println(jsonObject.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetInstanceInformation() {
|
||||
runBlocking {
|
||||
runTest {
|
||||
val callback = ProgressRecordTootApiCallback()
|
||||
val client = TootApiClient(
|
||||
appContext,
|
||||
|
@ -1135,8 +1150,8 @@ class TestTootApiClient {
|
|||
val instance = Host.parse("unit-test")
|
||||
client.apiHost = instance
|
||||
val (instanceInfo, instanceResult) = TootInstance.get(client)
|
||||
assertNotNull(instanceInfo)
|
||||
assertNotNull(instanceResult)
|
||||
assertNull("no error", instanceResult?.error)
|
||||
assertNotNull("instance info", instanceInfo)
|
||||
val json = instanceResult?.jsonObject
|
||||
if (json != null) println(json.toString())
|
||||
}
|
||||
|
@ -1144,7 +1159,7 @@ class TestTootApiClient {
|
|||
|
||||
@Test
|
||||
fun testGetHttp() {
|
||||
runBlocking {
|
||||
runTest {
|
||||
val callback = ProgressRecordTootApiCallback()
|
||||
val client = TootApiClient(
|
||||
appContext,
|
||||
|
@ -1160,7 +1175,7 @@ class TestTootApiClient {
|
|||
|
||||
@Test
|
||||
fun testRequest() {
|
||||
runBlocking {
|
||||
runTest {
|
||||
val tokenInfo = JsonObject()
|
||||
tokenInfo["access_token"] = "DUMMY_ACCESS_TOKEN"
|
||||
|
||||
|
@ -1188,7 +1203,7 @@ class TestTootApiClient {
|
|||
|
||||
@Test
|
||||
fun testWebSocket() {
|
||||
runBlocking {
|
||||
runTest {
|
||||
val tokenInfo = buildJsonObject {
|
||||
put("access_token", "DUMMY_ACCESS_TOKEN")
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package jp.juggler.subwaytooter.api.entity
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.util.data.*
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package jp.juggler.subwaytooter.api.entity
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import jp.juggler.subwaytooter.util.LinkHelper
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package jp.juggler.subwaytooter.database
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
|
@ -4,7 +4,7 @@ import android.content.Context
|
|||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import jp.juggler.subwaytooter.global.DB_VERSION
|
||||
import jp.juggler.subwaytooter.global.TABLE_LIST
|
||||
import org.junit.Assert.assertNull
|
||||
|
|
|
@ -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
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package jp.juggler.subwaytooter.util
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import jp.juggler.subwaytooter.api.entity.Host
|
||||
import jp.juggler.util.neatSpaces
|
||||
import jp.juggler.util.data.*
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
@ -17,7 +17,7 @@ class TestHtmlDecoder {
|
|||
val start: Int,
|
||||
val end: Int,
|
||||
val flags: Int,
|
||||
val text: String
|
||||
val text: String,
|
||||
) {
|
||||
override fun toString() = "[$start..$end) $flags ${span.javaClass.simpleName} $text"
|
||||
}
|
||||
|
|
|
@ -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.network.toFormRequestBody
|
||||
import jp.juggler.util.network.toPost
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
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.attrColor
|
||||
import jp.juggler.util.ui.createColoredDrawable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.anko.backgroundColor
|
||||
import java.lang.ref.WeakReference
|
||||
|
|
|
@ -8,6 +8,7 @@ import jp.juggler.subwaytooter.pref.PrefB
|
|||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.LinkHelper
|
||||
import jp.juggler.subwaytooter.util.VersionString
|
||||
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
|
||||
import jp.juggler.util.coroutine.launchDefault
|
||||
import jp.juggler.util.data.*
|
||||
import jp.juggler.util.log.LogCategory
|
||||
|
@ -15,7 +16,6 @@ import jp.juggler.util.log.withCaption
|
|||
import jp.juggler.util.network.toPostRequestBuilder
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import okhttp3.Request
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.coroutines.Continuation
|
||||
|
@ -431,7 +431,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
|
|||
try {
|
||||
val req = requestQueue.receive()
|
||||
val r = try {
|
||||
withTimeout(30000L) {
|
||||
withTimeoutSafe(30000L) {
|
||||
handleRequest(req)
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
|
@ -481,7 +481,7 @@ class TootInstance(parser: TootParser, src: JsonObject) {
|
|||
val cacheEntry = (hostArg ?: account?.apiHost ?: client.apiHost)?.getCacheEntry()
|
||||
?: return tiError("missing host.")
|
||||
|
||||
return withTimeout(30000L) {
|
||||
return withTimeoutSafe(30000L) {
|
||||
suspendCoroutine { cont ->
|
||||
QueuedRequest(cont, allowPixelfed) { cached ->
|
||||
|
||||
|
|
|
@ -8,11 +8,11 @@ import android.net.wifi.WifiManager
|
|||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
|
||||
import jp.juggler.util.log.*
|
||||
import jp.juggler.util.systemService
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeout
|
||||
|
||||
class CheckerWakeLocks(contextArg: Context) {
|
||||
companion object {
|
||||
|
@ -118,7 +118,7 @@ class CheckerWakeLocks(contextArg: Context) {
|
|||
suspend fun checkConnection() {
|
||||
var connectionState: String? = null
|
||||
try {
|
||||
withTimeout(10000L) {
|
||||
withTimeoutSafe(10000L) {
|
||||
while (true) {
|
||||
connectionState = appState.networkTracker.connectionState
|
||||
?: break // null if connected
|
||||
|
|
|
@ -29,7 +29,6 @@ import jp.juggler.util.network.toPostRequestBuilder
|
|||
import jp.juggler.util.network.toPut
|
||||
import jp.juggler.util.network.toPutRequestBuilder
|
||||
import jp.juggler.util.ui.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
|
@ -21,7 +21,6 @@ import jp.juggler.util.network.MEDIA_TYPE_JSON
|
|||
import jp.juggler.util.network.toPostRequestBuilder
|
||||
import jp.juggler.util.ui.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package jp.juggler.subwaytooter.util
|
||||
|
||||
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
|
||||
import jp.juggler.util.coroutine.launchDefault
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.withTimeout
|
||||
|
||||
abstract class WorkerBase(
|
||||
private val waiter: Channel<Unit> = Channel(capacity = Channel.CONFLATED),
|
||||
|
@ -17,7 +17,7 @@ abstract class WorkerBase(
|
|||
abstract suspend fun run()
|
||||
|
||||
suspend fun waitEx(ms: Long) = try {
|
||||
withTimeout(ms) { waiter.receive() }
|
||||
withTimeoutSafe(ms) { waiter.receive() }
|
||||
} catch (ignored: TimeoutCancellationException) {
|
||||
null
|
||||
}
|
||||
|
|
|
@ -111,11 +111,18 @@ dependencies {
|
|||
testApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
|
||||
|
||||
androidTestApi "androidx.test.espresso:espresso-core:3.5.1"
|
||||
androidTestApi "androidx.test.ext:junit-ktx:1.1.5"
|
||||
androidTestApi "androidx.test.ext:junit:1.1.5"
|
||||
androidTestApi "androidx.test.ext:truth:1.5.0"
|
||||
androidTestApi "androidx.test:core-ktx:1.5.0"
|
||||
androidTestApi "androidx.test:core:$androidx_test_version"
|
||||
androidTestApi "androidx.test:runner:1.5.2"
|
||||
androidTestApi "org.jetbrains.kotlin:kotlin-test"
|
||||
androidTestApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
|
||||
|
||||
// To use android test orchestrator
|
||||
androidTestUtil "androidx.test:orchestrator:1.4.2"
|
||||
|
||||
testApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") {
|
||||
exclude group: "com.squareup.okio", module: "okio"
|
||||
exclude group: "com.squareup.okhttp3", module: "okhttp"
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
package jp.juggler.base
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import jp.juggler.base.JugglerBase.Companion.jugglerBase
|
||||
import jp.juggler.base.JugglerBase.Companion.jugglerBaseNullable
|
||||
import jp.juggler.base.JugglerBase.Companion.prepareJugglerBase
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package jp.juggler.util.coroutine
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Test時にdispatcherを差し替えられるようにする
|
||||
|
@ -33,4 +32,15 @@ object AppDispatchers {
|
|||
default = testDispatcher
|
||||
io = testDispatcher
|
||||
}
|
||||
|
||||
/**
|
||||
* withTimeout はrunTest内部だと即座に例外を出すので、
|
||||
* テスト中はタイムアウトをチェックしないようにする
|
||||
* https://stackoverflow.com/questions/70658926/how-to-use-kotlinx-coroutines-withtimeout-in-kotlinx-coroutines-test-runtest
|
||||
*/
|
||||
suspend fun <T> withTimeoutSafe(timeMillis: Long, block: suspend CoroutineScope.() -> T) =
|
||||
when (io) {
|
||||
Dispatchers.IO -> withTimeout(timeMillis, block)
|
||||
else -> coroutineScope { block() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package jp.juggler.util.coroutine
|
|||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package jp.juggler.util.data
|
||||
|
||||
import java.util.LinkedHashMap
|
||||
|
||||
// same as x?.let{ dst.add(it) }
|
||||
fun <T> T.addTo(dst: ArrayList<T>) = dst.add(this)
|
||||
|
||||
|
@ -34,4 +32,3 @@ fun <T : Any> MutableCollection<T>.removeFirst(check: (T) -> Boolean): T? {
|
|||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,6 @@ import android.net.Uri
|
|||
import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.RawRes
|
||||
import jp.juggler.util.data.getStringOrNull
|
||||
import jp.juggler.util.data.notEmpty
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import okhttp3.internal.closeQuietly
|
||||
import java.io.InputStream
|
||||
|
|
|
@ -12,11 +12,11 @@ import android.widget.Toast
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
|
||||
import jp.juggler.util.coroutine.runOnMainLooper
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import me.drakeet.support.toast.ToastCompat
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.coroutines.resume
|
||||
|
@ -84,7 +84,7 @@ fun initializeToastUtils(app: Application) {
|
|||
*/
|
||||
suspend fun Animation.startAndAwait(duration: Long, v: View) =
|
||||
try {
|
||||
withTimeout(duration + 333L) {
|
||||
withTimeoutSafe(duration + 333L) {
|
||||
suspendCancellableCoroutine { cont ->
|
||||
v.clearAnimation()
|
||||
this@startAndAwait.duration = duration
|
||||
|
|
|
@ -37,7 +37,6 @@ internal class AlphaPatternDrawable(private val rectangleSize: Int) : Drawable()
|
|||
*/
|
||||
private var bitmap: Bitmap? = null
|
||||
|
||||
|
||||
/**
|
||||
* This will generate a bitmap with the pattern as big as the rectangle we were allow to draw on.
|
||||
* We do this to chache the bitmap so we don't need to recreate it each time draw() is called since it takes a few milliseconds
|
||||
|
|
|
@ -28,7 +28,7 @@ internal class ColorPaletteAdapter(
|
|||
val colors: IntArray,
|
||||
var selectedPosition: Int,
|
||||
@ColorShape val colorShape: Int,
|
||||
val listener: (Int)->Unit
|
||||
val listener: (Int) -> Unit,
|
||||
) : BaseAdapter() {
|
||||
|
||||
override fun getCount(): Int = colors.size
|
||||
|
|
|
@ -37,7 +37,7 @@ import androidx.core.view.ViewCompat
|
|||
class ColorPanelView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0
|
||||
defStyle: Int = 0,
|
||||
) : View(context, attrs, defStyle) {
|
||||
|
||||
companion object {
|
||||
|
@ -97,7 +97,6 @@ class ColorPanelView @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
val a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView)
|
||||
shape = a.getInt(R.styleable.ColorPanelView_cpv_colorShape, ColorShape.CIRCLE)
|
||||
|
@ -117,11 +116,8 @@ class ColorPanelView @JvmOverloads constructor(
|
|||
borderColor = typedArray.getColor(0, borderColor)
|
||||
typedArray.recycle()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public override fun onSaveInstanceState(): Parcelable {
|
||||
val state = Bundle()
|
||||
state.putParcelable("instanceState", super.onSaveInstanceState())
|
||||
|
@ -138,7 +134,6 @@ class ColorPanelView @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
borderPaint.color = borderColor
|
||||
colorPaint.color = color
|
||||
|
@ -236,7 +231,6 @@ class ColorPanelView @JvmOverloads constructor(
|
|||
alphaPattern.setBounds(left, top, right, bottom)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the original color. This is only used for previewing colors.
|
||||
*
|
||||
|
@ -264,13 +258,12 @@ class ColorPanelView @JvmOverloads constructor(
|
|||
val screenWidth = context.resources.displayMetrics.widthPixels
|
||||
referenceX = screenWidth - referenceX // mirror
|
||||
}
|
||||
val hint = StringBuilder("#")
|
||||
if (Color.alpha(color) != 255) {
|
||||
hint.append(Integer.toHexString(color).uppercase())
|
||||
} else {
|
||||
hint.append(String.format("%06X", 0xFFFFFF and color).uppercase())
|
||||
val hexText = when {
|
||||
Color.alpha(color) == 255 -> "%06X".format(color and 0xFFFFFF)
|
||||
else -> Integer.toHexString(color)
|
||||
}
|
||||
val cheatSheet = Toast.makeText(context, hint.toString(), Toast.LENGTH_SHORT)
|
||||
val hint = "#${hexText.uppercase()}"
|
||||
val cheatSheet = Toast.makeText(context, hint, Toast.LENGTH_SHORT)
|
||||
if (midy < displayFrame.height()) {
|
||||
// Show along the top; follow action buttons
|
||||
cheatSheet.setGravity(
|
||||
|
|
|
@ -265,11 +265,10 @@ class ColorPickerDialog :
|
|||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
|
||||
// region Custom Picker
|
||||
private fun createPickerView(): View {
|
||||
val args = arguments ?: throw RuntimeException("createPickerView: args is null")
|
||||
val activity = activity ?: throw RuntimeException("createPickerView: activity is null")
|
||||
val args = arguments ?: error("createPickerView: args is null")
|
||||
val activity = activity ?: error("createPickerView: activity is null")
|
||||
val contentView = View.inflate(getActivity(), R.layout.cpv_dialog_color_picker, null)
|
||||
colorPicker = contentView.findViewById(R.id.cpv_color_picker_view)
|
||||
val oldColorPanel: ColorPanelView = contentView.findViewById(R.id.cpv_color_panel_old)
|
||||
|
@ -376,14 +375,13 @@ class ColorPickerDialog :
|
|||
}
|
||||
|
||||
private fun setHex(color: Int) {
|
||||
if (showAlphaSlider) {
|
||||
hexEditText!!.setText(String.format("%08X", color))
|
||||
} else {
|
||||
hexEditText!!.setText(String.format("%06X", 0xFFFFFF and color))
|
||||
val hexText = when {
|
||||
showAlphaSlider -> "%08X".format(color)
|
||||
else -> "%06X".format(color and 0xFFFFFF)
|
||||
}
|
||||
hexEditText?.setText(hexText)
|
||||
}
|
||||
|
||||
|
||||
// -- endregion --
|
||||
// region Presets Picker
|
||||
private fun createPresetsView(): View {
|
||||
|
@ -519,18 +517,18 @@ class ColorPickerDialog :
|
|||
}
|
||||
|
||||
private fun shadeColor(@ColorInt color: Int, percent: Double): Int {
|
||||
val hex = String.format("#%06X", 0xFFFFFF and color)
|
||||
val hex = "#%06X".format(color and 0xFFFFFF)
|
||||
val f = hex.substring(1).toLong(16)
|
||||
val t = (if (percent < 0) 0 else 255).toDouble()
|
||||
val p = if (percent < 0) percent * -1 else percent
|
||||
val R = f shr 16
|
||||
val G = f shr 8 and 0x00FF
|
||||
val B = f and 0x0000FF
|
||||
val cR = f shr 16
|
||||
val cG = f shr 8 and 0x00FF
|
||||
val cB = f and 0x0000FF
|
||||
return Color.argb(
|
||||
Color.alpha(color),
|
||||
((t - R) * p).roundToInt() + R.toInt(),
|
||||
((t - G) * p).roundToInt() + G.toInt(),
|
||||
((t - B) * p).roundToInt() + B.toInt(),
|
||||
((t - cR) * p).roundToInt() + cR.toInt(),
|
||||
((t - cG) * p).roundToInt() + cG.toInt(),
|
||||
((t - cB) * p).roundToInt() + cB.toInt(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -502,13 +502,13 @@ class ColorPickerView @JvmOverloads constructor(
|
|||
return p
|
||||
}
|
||||
|
||||
private fun satValToPoint(sat: Float, `val`: Float): Point {
|
||||
val rect = satValRect
|
||||
val height = rect!!.height().toFloat()
|
||||
private fun satValToPoint(sat: Float, inValue: Float): Point {
|
||||
val rect = satValRect!!
|
||||
val height = rect.height().toFloat()
|
||||
val width = rect.width().toFloat()
|
||||
val p = Point()
|
||||
p.x = (sat * width + rect.left).toInt()
|
||||
p.y = ((1f - `val`) * height + rect.top).toInt()
|
||||
p.y = ((1f - inValue) * height + rect.top).toInt()
|
||||
return p
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == PERMISSION_REQUEST_CODE_STORAGE) {
|
||||
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
|
||||
// 特に何もしてないらしい
|
||||
}
|
||||
}
|
||||
// if (requestCode == PERMISSION_REQUEST_CODE_STORAGE) {
|
||||
// if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
|
||||
// // 特に何もしてないらしい
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private fun load() = launch {
|
||||
|
@ -153,14 +153,14 @@ class ActList : AppCompatActivity(), CoroutineScope {
|
|||
|
||||
inner class MyViewHolder(
|
||||
viewRoot: View,
|
||||
_activity: ActList,
|
||||
actList: ActList,
|
||||
) {
|
||||
|
||||
private val tvCaption: TextView = viewRoot.findViewById(R.id.tvCaption)
|
||||
private val apngView: ApngView = viewRoot.findViewById(R.id.apngView)
|
||||
|
||||
init {
|
||||
apngView.timeAnimationStart = _activity.timeAnimationStart
|
||||
apngView.timeAnimationStart = actList.timeAnimationStart
|
||||
}
|
||||
|
||||
private var lastId: Int = 0
|
||||
|
|
|
@ -125,7 +125,7 @@ class ActViewer : AsyncActivity() {
|
|||
Log.d(TAG, "$title[$i] timeWidth=${f.timeWidth}")
|
||||
val bitmap = f.bitmap
|
||||
|
||||
FileOutputStream(File(dir, "${title}_${i}.png")).use { fo ->
|
||||
FileOutputStream(File(dir, "${title}_$i.png")).use { fo ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fo)
|
||||
}
|
||||
|
||||
|
|
|
@ -23,10 +23,16 @@ class ApngView : View{
|
|||
constructor(context: Context) : super(context, null) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, 0) {
|
||||
init(context)
|
||||
}
|
||||
constructor(context : Context, attrs : AttributeSet?, defStyle : Int) : super(context, attrs, defStyle) {
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyle
|
||||
) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
|
@ -42,22 +48,17 @@ class ApngView : View{
|
|||
|
||||
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) {
|
||||
//
|
||||
}
|
||||
|
@ -90,7 +91,6 @@ class ApngView : View{
|
|||
|
||||
currentTransX = (wView - wDraw) / 2f
|
||||
currentTransY = (hView - hDraw) / 2f
|
||||
|
||||
} else {
|
||||
currentScale = 1f
|
||||
currentTransX = 0f
|
||||
|
@ -99,7 +99,6 @@ class ApngView : View{
|
|||
invalidate()
|
||||
}
|
||||
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
|
@ -124,5 +123,4 @@ class ApngView : View{
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue