1040 lines
29 KiB
Kotlin
1040 lines
29 KiB
Kotlin
package jp.juggler.subwaytooter.util
|
|
|
|
import android.app.Activity
|
|
import android.content.ContentResolver
|
|
import android.content.Context
|
|
import android.content.ContextWrapper
|
|
import android.content.Intent
|
|
import android.content.res.Resources
|
|
import android.database.Cursor
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.Handler
|
|
import android.os.Looper
|
|
import android.provider.OpenableColumns
|
|
import android.text.Spannable
|
|
import android.text.SpannableString
|
|
import android.text.SpannableStringBuilder
|
|
import android.util.Base64
|
|
import android.util.SparseBooleanArray
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.view.inputmethod.InputMethodManager
|
|
import android.webkit.MimeTypeMap
|
|
import android.widget.Toast
|
|
import jp.juggler.subwaytooter.api.TootApiClient
|
|
import me.drakeet.support.toast.ToastCompat
|
|
import okhttp3.Request
|
|
import okhttp3.RequestBody
|
|
import org.apache.commons.io.IOUtils
|
|
import org.json.JSONArray
|
|
import org.json.JSONObject
|
|
import java.io.*
|
|
import java.lang.ref.WeakReference
|
|
import java.security.MessageDigest
|
|
import java.util.LinkedList
|
|
import java.util.Locale
|
|
import java.util.regex.Matcher
|
|
import java.util.regex.Pattern
|
|
import kotlin.collections.ArrayList
|
|
import kotlin.collections.HashMap
|
|
import kotlin.collections.set
|
|
|
|
object Utils {
|
|
|
|
val log = LogCategory("Utils")
|
|
|
|
val hexLower =
|
|
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
|
|
|
|
/////////////////////////////////////////////
|
|
|
|
private val taisaku_map : HashMap<Char, String>
|
|
private val taisaku_map2 : SparseBooleanArray
|
|
|
|
// public static int getEnumStringId( String residPrefix, String name,Context context ) {
|
|
// name = residPrefix + name;
|
|
// try{
|
|
// int iv = context.getResources().getIdentifier(name,"string",context.getPackageName() );
|
|
// if( iv != 0 ) return iv;
|
|
// }catch(Throwable ex){
|
|
// }
|
|
// warning.e("missing resid for %s",name);
|
|
// return R.string.Dialog_Cancel;
|
|
// }
|
|
|
|
// public static String getConnectionResultErrorMessage( ConnectionResult connectionResult ){
|
|
// int code = connectionResult.getErrorCode();
|
|
// String msg = connectionResult.getErrorMessage();
|
|
// if( msg == null || msg.isEmpty( ) ){
|
|
// switch( code ){
|
|
// case ConnectionResult.SUCCESS:
|
|
// msg = "SUCCESS";
|
|
// break;
|
|
// case ConnectionResult.SERVICE_MISSING:
|
|
// msg = "SERVICE_MISSING";
|
|
// break;
|
|
// case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
|
|
// msg = "SERVICE_VERSION_UPDATE_REQUIRED";
|
|
// break;
|
|
// case ConnectionResult.SERVICE_DISABLED:
|
|
// msg = "SERVICE_DISABLED";
|
|
// break;
|
|
// case ConnectionResult.SIGN_IN_REQUIRED:
|
|
// msg = "SIGN_IN_REQUIRED";
|
|
// break;
|
|
// case ConnectionResult.INVALID_ACCOUNT:
|
|
// msg = "INVALID_ACCOUNT";
|
|
// break;
|
|
// case ConnectionResult.RESOLUTION_REQUIRED:
|
|
// msg = "RESOLUTION_REQUIRED";
|
|
// break;
|
|
// case ConnectionResult.NETWORK_ERROR:
|
|
// msg = "NETWORK_ERROR";
|
|
// break;
|
|
// case ConnectionResult.INTERNAL_ERROR:
|
|
// msg = "INTERNAL_ERROR";
|
|
// break;
|
|
// case ConnectionResult.SERVICE_INVALID:
|
|
// msg = "SERVICE_INVALID";
|
|
// break;
|
|
// case ConnectionResult.DEVELOPER_ERROR:
|
|
// msg = "DEVELOPER_ERROR";
|
|
// break;
|
|
// case ConnectionResult.LICENSE_CHECK_FAILED:
|
|
// msg = "LICENSE_CHECK_FAILED";
|
|
// break;
|
|
// case ConnectionResult.CANCELED:
|
|
// msg = "CANCELED";
|
|
// break;
|
|
// case ConnectionResult.TIMEOUT:
|
|
// msg = "TIMEOUT";
|
|
// break;
|
|
// case ConnectionResult.INTERRUPTED:
|
|
// msg = "INTERRUPTED";
|
|
// break;
|
|
// case ConnectionResult.API_UNAVAILABLE:
|
|
// msg = "API_UNAVAILABLE";
|
|
// break;
|
|
// case ConnectionResult.SIGN_IN_FAILED:
|
|
// msg = "SIGN_IN_FAILED";
|
|
// break;
|
|
// case ConnectionResult.SERVICE_UPDATING:
|
|
// msg = "SERVICE_UPDATING";
|
|
// break;
|
|
// case ConnectionResult.SERVICE_MISSING_PERMISSION:
|
|
// msg = "SERVICE_MISSING_PERMISSION";
|
|
// break;
|
|
// case ConnectionResult.RESTRICTED_PROFILE:
|
|
// msg = "RESTRICTED_PROFILE";
|
|
// break;
|
|
//
|
|
// }
|
|
// }
|
|
// return msg;
|
|
// }
|
|
|
|
// public static String getConnectionSuspendedMessage( int i ){
|
|
// switch( i ){
|
|
// default:
|
|
// return "?";
|
|
// case GoogleApiClient.ConnectionCallbacks.CAUSE_NETWORK_LOST:
|
|
// return "NETWORK_LOST";
|
|
// case GoogleApiClient.ConnectionCallbacks.CAUSE_SERVICE_DISCONNECTED:
|
|
// return "SERVICE_DISCONNECTED";
|
|
// }
|
|
// }
|
|
|
|
private const val MIME_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream"
|
|
|
|
// BDI制御文字からその制御文字を閉じる文字を得るためのマップ
|
|
val sanitizeBdiMap = HashMap<Char, Char>().apply {
|
|
|
|
val PDF = 0x202C.toChar() // Pop directional formatting (PDF)
|
|
this[0x202A.toChar()] = PDF // Left-to-right embedding (LRE)
|
|
this[0x202B.toChar()] = PDF // Right-to-left embedding (RLE)
|
|
this[0x202D.toChar()] = PDF // Left-to-right override (LRO)
|
|
this[0x202E.toChar()] = PDF // Right-to-left override (RLO)
|
|
|
|
val PDI = 0x2069.toChar() // Pop directional isolate (PDI)
|
|
this[0x2066.toChar()] = PDI // Left-to-right isolate (LRI)
|
|
this[0x2067.toChar()] = PDI // Right-to-left isolate (RLI)
|
|
this[0x2068.toChar()] = PDI // First strong isolate (FSI)
|
|
|
|
// private const val ALM = 0x061c.toChar() // Arabic letter mark (ALM)
|
|
// private const val LRM = 0x200E.toChar() // Left-to-right mark (LRM)
|
|
// private const val RLM = 0x200F.toChar() // Right-to-left mark (RLM)
|
|
}
|
|
|
|
private var refToast : WeakReference<Toast>? = null
|
|
|
|
internal fun showToastImpl(context : Context, bLong : Boolean, message : String) {
|
|
runOnMainLooper {
|
|
|
|
// 前回のトーストの表示を終了する
|
|
try {
|
|
refToast?.get()?.cancel()
|
|
} catch(ex : Throwable) {
|
|
log.trace(ex)
|
|
} finally {
|
|
refToast = null
|
|
}
|
|
|
|
// 新しいトーストを作る
|
|
try {
|
|
val duration = if(bLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
|
|
val t = ToastCompat.makeText(context, message, duration)
|
|
t.setBadTokenListener {}
|
|
t.show()
|
|
refToast = WeakReference(t)
|
|
} catch(ex : Throwable) {
|
|
log.trace(ex)
|
|
}
|
|
|
|
// コールスタックの外側でエラーになる…
|
|
// android.view.WindowManager$BadTokenException:
|
|
// at android.view.ViewRootImpl.setView (ViewRootImpl.java:679)
|
|
// at android.view.WindowManagerGlobal.addView (WindowManagerGlobal.java:342)
|
|
// at android.view.WindowManagerImpl.addView (WindowManagerImpl.java:94)
|
|
// at android.widget.Toast$TN.handleShow (Toast.java:435)
|
|
// at android.widget.Toast$TN$2.handleMessage (Toast.java:345)
|
|
}
|
|
}
|
|
|
|
// fun url2name(url : String?) : String? {
|
|
// return if(url == null) null else encodeBase64Url(encodeSHA256(encodeUTF8(url)))
|
|
// }
|
|
|
|
// public static String name2url(String entry) {
|
|
// if(entry==null) return null;
|
|
// byte[] b = new byte[entry.length()/2];
|
|
// for(int i=0,ie=b.length;i<ie;++i){
|
|
// b[i]= (byte)((hex2int(entry.charAt(i*2))<<4)| hex2int(entry.charAt(i*2+1)));
|
|
// }
|
|
// return decodeUTF8(b);
|
|
// }
|
|
|
|
///////////////////////////////////////////////////
|
|
|
|
private fun _taisaku_add_string(z : String, h : String) {
|
|
var i = 0
|
|
val e = z.length
|
|
while(i < e) {
|
|
val zc = z[i]
|
|
taisaku_map[zc] = h[i].toString()
|
|
taisaku_map2.put(zc.toInt(), true)
|
|
++ i
|
|
}
|
|
}
|
|
|
|
init {
|
|
taisaku_map = HashMap()
|
|
taisaku_map2 = SparseBooleanArray()
|
|
|
|
// tilde,wave dash,horizontal ellipsis,minus sign
|
|
_taisaku_add_string(
|
|
"\u2073\u301C\u22EF\uFF0D", "\u007e\uFF5E\u2026\u2212"
|
|
)
|
|
// zenkaku to hankaku
|
|
_taisaku_add_string(
|
|
" !”#$%&’()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}",
|
|
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}"
|
|
)
|
|
|
|
}
|
|
|
|
private fun isBadChar2(c : Char) : Boolean {
|
|
return c.toInt() == 0xa || taisaku_map2.get(c.toInt())
|
|
}
|
|
|
|
//! フォントによって全角文字が化けるので、その対策
|
|
@Suppress("unused")
|
|
fun font_taisaku(text : String?, lf2br : Boolean) : String? {
|
|
if(text == null) return null
|
|
val l = text.length
|
|
val sb = StringBuilder(l)
|
|
if(! lf2br) {
|
|
var i = 0
|
|
while(i < l) {
|
|
val start = i
|
|
while(i < l && ! taisaku_map2.get(text[i].toInt())) ++ i
|
|
if(i > start) {
|
|
sb.append(text.substring(start, i))
|
|
if(i >= l) break
|
|
}
|
|
sb.append(taisaku_map[text[i]])
|
|
++ i
|
|
}
|
|
} else {
|
|
var i = 0
|
|
while(i < l) {
|
|
val start = i
|
|
while(i < l && ! isBadChar2(text[i])) ++ i
|
|
if(i > start) {
|
|
sb.append(text.substring(start, i))
|
|
if(i >= l) break
|
|
}
|
|
val c = text[i]
|
|
if(c.toInt() == 0xa) {
|
|
sb.append("<br/>")
|
|
} else {
|
|
sb.append(taisaku_map[c])
|
|
}
|
|
++ i
|
|
}
|
|
}
|
|
return sb.toString()
|
|
}
|
|
|
|
////////////////////////////
|
|
|
|
private val mimeTypeExMap : HashMap<String, String> by lazy {
|
|
val map = HashMap<String, String>()
|
|
map["BDM"] = "application/vnd.syncml.dm+wbxml"
|
|
map["DAT"] = ""
|
|
map["TID"] = ""
|
|
map["js"] = "text/javascript"
|
|
map["sh"] = "application/x-sh"
|
|
map["lua"] = "text/x-lua"
|
|
map
|
|
}
|
|
|
|
@Suppress("unused")
|
|
fun getMimeType(log : LogCategory?, src : String) : String {
|
|
var ext = MimeTypeMap.getFileExtensionFromUrl(src)
|
|
if(ext != null && ext.isNotEmpty()) {
|
|
ext = ext.toLowerCase(Locale.US)
|
|
|
|
//
|
|
var mime_type : String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
|
|
if(mime_type?.isNotEmpty() == true) return mime_type
|
|
|
|
//
|
|
mime_type = mimeTypeExMap[ext]
|
|
if(mime_type?.isNotEmpty() == true) return mime_type
|
|
|
|
// 戻り値が空文字列の場合とnullの場合があり、空文字列の場合は既知なのでログ出力しない
|
|
|
|
if(mime_type == null && log != null) {
|
|
log.w("getMimeType(): unknown file extension '%s'", ext)
|
|
}
|
|
}
|
|
return MIME_TYPE_APPLICATION_OCTET_STREAM
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Comparable
|
|
|
|
fun <T : Comparable<T>> clipRange(min : T, max : T, src : T) =
|
|
if(src < min) min else if(src > max) max else src
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// ByteArray
|
|
|
|
// 16進ダンプ
|
|
private fun ByteArray.encodeHex() : String {
|
|
val sb = StringBuilder()
|
|
for(b in this) {
|
|
sb.appendHex2(b.toInt())
|
|
}
|
|
return sb.toString()
|
|
}
|
|
|
|
fun ByteArray.encodeBase64Url() : String =
|
|
Base64.encodeToString(this, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
|
|
|
fun ByteArray.digestSHA256() : ByteArray {
|
|
val digest = MessageDigest.getInstance("SHA-256")
|
|
digest.reset()
|
|
return digest.digest(this)
|
|
}
|
|
|
|
fun ByteArray.startWith(
|
|
key:ByteArray,
|
|
thisOffset :Int =0,
|
|
keyOffset:Int=0,
|
|
length:Int = key.size-keyOffset
|
|
):Boolean{
|
|
if( this.size -thisOffset >= length && key.size -keyOffset >=length ){
|
|
for( i in 0 until length ){
|
|
if( this[i+thisOffset] != key[i+keyOffset]) return false
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 各要素の下位8ビットを使ってバイト配列を作る
|
|
fun IntArray.toByteArray():ByteArray{
|
|
val dst = ByteArray(this.size)
|
|
for(i in 0 until this.size){
|
|
dst[i] = this[i].toByte()
|
|
}
|
|
return dst
|
|
}
|
|
|
|
// 各要素の下位8ビットを使ってバイト配列を作る
|
|
fun CharArray.toByteArray():ByteArray{
|
|
val dst = ByteArray(this.size)
|
|
for(i in 0 until this.size){
|
|
dst[i] = this[i].toByte()
|
|
}
|
|
return dst
|
|
}
|
|
|
|
|
|
//// MD5ハッシュの作成
|
|
//@Suppress("unused")
|
|
//fun String.digestMD5() : String {
|
|
// val md = MessageDigest.getInstance("MD5")
|
|
// md.reset()
|
|
// return md.digest(this.encodeUTF8()).encodeHex()
|
|
//}
|
|
|
|
fun String.digestSHA256Hex() : String {
|
|
return this.encodeUTF8().digestSHA256().encodeHex()
|
|
}
|
|
|
|
fun String.digestSHA256Base64Url() : String {
|
|
return this.encodeUTF8().digestSHA256().encodeBase64Url()
|
|
}
|
|
|
|
fun String.toUri():Uri = Uri.parse(this)
|
|
|
|
fun String.unescapeUri():String = Uri.decode(this)
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// CharSequence
|
|
|
|
fun CharSequence.replaceFirst(pattern : Pattern, replacement : String) : String {
|
|
return pattern.matcher(this).replaceFirst(replacement)
|
|
// replaceFirstの戻り値がplatform type なので expression body 形式にすると警告がでる
|
|
}
|
|
|
|
fun CharSequence.replaceAll(pattern : Pattern, replacement : String) : String {
|
|
return pattern.matcher(this).replaceAll(replacement)
|
|
// replaceAllの戻り値がplatform type なので expression body 形式にすると警告がでる
|
|
}
|
|
|
|
// %1$s を含む文字列リソースを利用して装飾テキストの前後に文字列を追加する
|
|
fun CharSequence?.intoStringResource(context : Context, string_id : Int) : Spannable {
|
|
|
|
val s = context.getString(string_id)
|
|
val end = s.length
|
|
val pos = s.indexOf("%1\$s")
|
|
if(pos == - 1) return SpannableString(s)
|
|
|
|
val sb = SpannableStringBuilder()
|
|
if(pos > 0) sb.append(s.substring(0, pos))
|
|
if(this != null) sb.append(this)
|
|
if(pos + 4 < end) sb.append(s.substring(pos + 4, end))
|
|
return sb
|
|
}
|
|
|
|
//fun Char.hex2int() : Int {
|
|
// if( '0' <= this && this <= '9') return ((this-'0'))
|
|
// if( 'A' <= this && this <= 'F') return ((this-'A')+0xa)
|
|
// if( 'a' <= this && this <= 'f') return ((this-'a')+0xa)
|
|
// return 0
|
|
//}
|
|
|
|
fun CharSequence.codePointBefore(index : Int) : Int {
|
|
if(index > 0) {
|
|
val c2 = this[index - 1]
|
|
if(Character.isLowSurrogate(c2) && index > 1) {
|
|
val c1 = this[index - 2]
|
|
if(Character.isHighSurrogate(c1)) return Character.toCodePoint(c1, c2)
|
|
}
|
|
return c2.toInt()
|
|
} else {
|
|
return - 1
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// string
|
|
|
|
val charsetUTF8 = Charsets.UTF_8
|
|
|
|
// 文字列とバイト列の変換
|
|
fun String.encodeUTF8() = this.toByteArray(charsetUTF8)
|
|
|
|
fun ByteArray.decodeUTF8() = this.toString(charsetUTF8)
|
|
|
|
fun StringBuilder.appendHex2(value : Int) : StringBuilder {
|
|
this.append(Utils.hexLower[(value shr 4) and 15])
|
|
this.append(Utils.hexLower[value and 15])
|
|
return this
|
|
}
|
|
|
|
fun ByteArray.encodeHexLower() : String {
|
|
val size = this.size
|
|
val sb = StringBuilder(size * 2)
|
|
for(i in 0 until size) {
|
|
val value = this[i].toInt()
|
|
sb.append(Utils.hexLower[(value shr 4) and 15])
|
|
sb.append(Utils.hexLower[value and 15])
|
|
}
|
|
return sb.toString()
|
|
}
|
|
|
|
fun String?.optInt() : Int? {
|
|
return try {
|
|
this?.toInt(10)
|
|
} catch(ignored : Throwable) {
|
|
null
|
|
}
|
|
}
|
|
|
|
//fun String.ellipsize(max : Int) = if(this.length > max) this.substring(0, max - 1) + "…" else this
|
|
//
|
|
//fun String.toCamelCase() : String {
|
|
// val sb = StringBuilder()
|
|
// for(s in this.split("_".toRegex())) {
|
|
// if(s.isEmpty()) continue
|
|
// sb.append(Character.toUpperCase(s[0]))
|
|
// sb.append(s.substring(1, s.length).toLowerCase())
|
|
// }
|
|
// return sb.toString()
|
|
//}
|
|
|
|
fun String.sanitizeBDI() : String {
|
|
|
|
// 文字列をスキャンしてBDI制御文字をスタックに入れていく
|
|
var stack : LinkedList<Char>? = null
|
|
for(c in this) {
|
|
val closer = Utils.sanitizeBdiMap[c]
|
|
if(closer != null) {
|
|
if(stack == null) stack = LinkedList()
|
|
stack.add(closer)
|
|
} else if(stack?.isNotEmpty() == true && stack.last == c) {
|
|
stack.removeLast()
|
|
}
|
|
}
|
|
|
|
if(stack?.isNotEmpty() == true) {
|
|
val sb = StringBuilder(this.length + stack.size)
|
|
sb.append(this)
|
|
while(! stack.isEmpty()) {
|
|
sb.append(stack.removeLast())
|
|
}
|
|
return sb.toString()
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
// Uri.encode(s:Nullable) だと nullチェックができないので、簡単なラッパーを用意する
|
|
fun String.encodePercent(allow : String? = null) : String = Uri.encode(this, allow)
|
|
|
|
//fun String.dumpCodePoints() : CharSequence {
|
|
// val sb = StringBuilder()
|
|
// val length = this.length
|
|
// var i=0
|
|
// while(i<length) {
|
|
// val cp = codePointAt(i)
|
|
// sb.append(String.format("0x%x,", cp))
|
|
// i += Character.charCount(cp)
|
|
// }
|
|
// return sb
|
|
//}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// long
|
|
|
|
//@SuppressLint("DefaultLocale")
|
|
//fun Long.formatTimeDuration() : String {
|
|
// var t = this
|
|
// val sb = StringBuilder()
|
|
// var n : Long
|
|
// // day
|
|
// n = t / 86400000L
|
|
// if(n > 0) {
|
|
// sb.append(String.format(Locale.JAPAN, "%dd", n))
|
|
// t -= n * 86400000L
|
|
// }
|
|
// // h
|
|
// n = t / 3600000L
|
|
// if(n > 0 || sb.isNotEmpty()) {
|
|
// sb.append(String.format(Locale.JAPAN, "%dh", n))
|
|
// t -= n * 3600000L
|
|
// }
|
|
// // m
|
|
// n = t / 60000L
|
|
// if(n > 0 || sb.isNotEmpty()) {
|
|
// sb.append(String.format(Locale.JAPAN, "%dm", n))
|
|
// t -= n * 60000L
|
|
// }
|
|
// // s
|
|
// val f = t / 1000f
|
|
// sb.append(String.format(Locale.JAPAN, "%.03fs", f))
|
|
//
|
|
// return sb.toString()
|
|
//}
|
|
|
|
//private val bytesSizeFormat = DecimalFormat("#,###")
|
|
//fun Long.formatBytesSize() = Utils.bytesSizeFormat.format(this)
|
|
|
|
// StringBuilder sb = new StringBuilder();
|
|
// long n;
|
|
// // giga
|
|
// n = t / 1000000000L;
|
|
// if( n > 0 ){
|
|
// sb.append( String.format( Locale.JAPAN, "%dg", n ) );
|
|
// t -= n * 1000000000L;
|
|
// }
|
|
// // Mega
|
|
// n = t / 1000000L;
|
|
// if( sb.length() > 0 ){
|
|
// sb.append( String.format( Locale.JAPAN, "%03dm", n ) );
|
|
// t -= n * 1000000L;
|
|
// }else if( n > 0 ){
|
|
// sb.append( String.format( Locale.JAPAN, "%dm", n ) );
|
|
// t -= n * 1000000L;
|
|
// }
|
|
// // kilo
|
|
// n = t / 1000L;
|
|
// if( sb.length() > 0 ){
|
|
// sb.append( String.format( Locale.JAPAN, "%03dk", n ) );
|
|
// t -= n * 1000L;
|
|
// }else if( n > 0 ){
|
|
// sb.append( String.format( Locale.JAPAN, "%dk", n ) );
|
|
// t -= n * 1000L;
|
|
// }
|
|
// // length
|
|
// if( sb.length() > 0 ){
|
|
// sb.append( String.format( Locale.JAPAN, "%03d", t ) );
|
|
// }else if( n > 0 ){
|
|
// sb.append( String.format( Locale.JAPAN, "%d", t ) );
|
|
// }
|
|
//
|
|
// return sb.toString();
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// JSON
|
|
|
|
fun removeJsonNull(o : Any?) = if(JSONObject.NULL === o) null else o
|
|
|
|
// for in でループを回せるようにする。
|
|
// インライン展開できずIteratorを生成
|
|
fun JSONArray.iterator() : Iterator<Any?> {
|
|
val self = this
|
|
val end = length()
|
|
return object : Iterator<Any?> {
|
|
var i = 0
|
|
override fun hasNext() : Boolean = i < end
|
|
override fun next() : Any? = self.opt(i ++)
|
|
}
|
|
}
|
|
|
|
fun JSONArray.reverseIterator() : Iterator<Any?> {
|
|
val self = this
|
|
return object : Iterator<Any?> {
|
|
var i = length() - 1
|
|
override fun hasNext() : Boolean = i >= 0
|
|
override fun next() : Any? = self.opt(i --)
|
|
}
|
|
}
|
|
|
|
inline fun JSONArray.forEach(block : (v : Any?) -> Unit) {
|
|
val e = this.length()
|
|
var i = 0
|
|
while(i < e) {
|
|
block(removeJsonNull(this.opt(i)))
|
|
++ i
|
|
}
|
|
}
|
|
|
|
inline fun JSONArray.forEachIndexed(block : (i : Int, v : Any?) -> Unit) {
|
|
val e = this.length()
|
|
var i = 0
|
|
while(i < e) {
|
|
block(i, removeJsonNull(this.opt(i)))
|
|
++ i
|
|
}
|
|
}
|
|
|
|
inline fun JSONArray.downForEach(block : (v : Any?) -> Unit) {
|
|
var i = this.length() - 1
|
|
while(i >= 0) {
|
|
block(removeJsonNull(this.opt(i)))
|
|
-- i
|
|
}
|
|
}
|
|
|
|
inline fun JSONArray.downForEachIndexed(block : (i : Int, v : Any?) -> Unit) {
|
|
var i = this.length() - 1
|
|
while(i >= 0) {
|
|
block(i, removeJsonNull(this.opt(i)))
|
|
-- i
|
|
}
|
|
}
|
|
|
|
fun JSONArray.toStringArrayList() : ArrayList<String> {
|
|
val dst_list = ArrayList<String>(length())
|
|
forEach { o ->
|
|
val sv = o?.toString()
|
|
if(sv != null) dst_list.add(sv)
|
|
}
|
|
return dst_list
|
|
}
|
|
|
|
fun String.toJsonObject() = JSONObject(this)
|
|
fun String.toJsonArray() = JSONArray(this)
|
|
|
|
fun JSONObject.parseString(key : String) : String? {
|
|
val o = this.opt(key)
|
|
return if(o == null || o == JSONObject.NULL) null else o.toString()
|
|
}
|
|
|
|
fun JSONArray.parseString(key : Int) : String? {
|
|
val o = this.opt(key)
|
|
return if(o == null || o == JSONObject.NULL) null else o.toString()
|
|
}
|
|
|
|
fun notEmptyOrThrow(name : String, value : String?) =
|
|
if(value?.isNotEmpty() == true) value else throw RuntimeException("$name is empty")
|
|
|
|
fun JSONObject.notEmptyOrThrow(name : String) = notEmptyOrThrow(name, this.parseString(name))
|
|
|
|
// 文字列データをLong精度で取得できる代替品
|
|
// (JsonObject.optLong はLong精度が出ない)
|
|
fun JSONObject.parseLong(key : String) : Long? {
|
|
val o = this.opt(key)
|
|
return when(o) {
|
|
is Long -> return o
|
|
is Number -> return o.toLong()
|
|
|
|
is String -> {
|
|
if(o.indexOf('.') == - 1 && o.indexOf(',') == - 1) {
|
|
try {
|
|
return o.toLong(10)
|
|
} catch(ignored : NumberFormatException) {
|
|
}
|
|
}
|
|
try {
|
|
o.toDouble().toLong()
|
|
} catch(ignored : NumberFormatException) {
|
|
null
|
|
}
|
|
}
|
|
|
|
else -> null // may null or JSONObject.NULL or object,array,boolean
|
|
}
|
|
}
|
|
|
|
fun JSONObject.parseInt(key : String) : Int? {
|
|
val o = this.opt(key)
|
|
return when(o) {
|
|
is Int -> return o
|
|
|
|
is Number -> return try {
|
|
o.toInt()
|
|
} catch(ignored : NumberFormatException) {
|
|
null
|
|
}
|
|
|
|
is String -> {
|
|
if(o.indexOf('.') == - 1 && o.indexOf(',') == - 1) {
|
|
try {
|
|
return o.toInt(10)
|
|
} catch(ignored : NumberFormatException) {
|
|
}
|
|
}
|
|
try {
|
|
o.toDouble().toInt()
|
|
} catch(ignored : NumberFormatException) {
|
|
null
|
|
}
|
|
}
|
|
|
|
else -> null // may null or JSONObject.NULL or object,array,boolean
|
|
}
|
|
}
|
|
|
|
fun JSONObject.toPostRequestBuilder() : Request.Builder =
|
|
Request.Builder().post(RequestBody.create(TootApiClient.MEDIA_TYPE_JSON, this.toString()))
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Bundle
|
|
|
|
fun Bundle.parseString(key : String) : String? {
|
|
return try {
|
|
this.getString(key)
|
|
} catch(ignored : Throwable) {
|
|
null
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// Throwable
|
|
|
|
fun Throwable.withCaption(fmt : String?, vararg args : Any) =
|
|
"${
|
|
if(fmt == null || args.isEmpty())
|
|
fmt
|
|
else
|
|
String.format(fmt, *args)
|
|
}: ${this.javaClass.simpleName} ${this.message}"
|
|
|
|
fun Throwable.withCaption(resources : Resources, string_id : Int, vararg args : Any) =
|
|
"${
|
|
resources.getString(string_id, *args)
|
|
}: ${this.javaClass.simpleName} ${this.message}"
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// threading
|
|
|
|
val isMainThread : Boolean get() = Looper.getMainLooper().thread === Thread.currentThread()
|
|
|
|
fun runOnMainLooper(proc : () -> Unit) {
|
|
val looper = Looper.getMainLooper()
|
|
if(looper.thread === Thread.currentThread()) {
|
|
proc()
|
|
} else {
|
|
Handler(looper).post { proc() }
|
|
}
|
|
}
|
|
|
|
fun runOnMainLooperDelayed(delayMs : Long, proc : () -> Unit) {
|
|
val looper = Looper.getMainLooper()
|
|
Handler(looper).postDelayed({ proc() }, delayMs)
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// View
|
|
|
|
fun View?.scan(callback : (view : View) -> Unit) {
|
|
this ?: return
|
|
callback(this)
|
|
if(this is ViewGroup) {
|
|
for(i in 0 until this.childCount) {
|
|
this.getChildAt(i)?.scan(callback)
|
|
}
|
|
}
|
|
}
|
|
|
|
val View?.activity : Activity?
|
|
get() {
|
|
var context = this?.context
|
|
while(context is ContextWrapper) {
|
|
if(context is Activity) return context
|
|
context = context.baseContext
|
|
}
|
|
return null
|
|
}
|
|
|
|
fun View.hideKeyboard() {
|
|
try {
|
|
val imm = this.context?.getSystemService(Context.INPUT_METHOD_SERVICE)
|
|
if(imm is InputMethodManager) {
|
|
imm.hideSoftInputFromWindow(this.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
|
} else {
|
|
Utils.log.e("hideKeyboard: can't get InputMethodManager")
|
|
}
|
|
} catch(ex : Throwable) {
|
|
Utils.log.trace(ex)
|
|
}
|
|
}
|
|
|
|
fun View.showKeyboard() {
|
|
try {
|
|
val imm = this.context?.getSystemService(Context.INPUT_METHOD_SERVICE)
|
|
if(imm is InputMethodManager) {
|
|
imm.showSoftInput(this, InputMethodManager.HIDE_NOT_ALWAYS)
|
|
} else {
|
|
Utils.log.e("showKeyboard: can't get InputMethodManager")
|
|
}
|
|
} catch(ex : Throwable) {
|
|
Utils.log.trace(ex)
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// context
|
|
|
|
fun Context.loadRawResource( resId:Int):ByteArray{
|
|
resources.openRawResource(resId).use{ inStream->
|
|
val bao = ByteArrayOutputStream( inStream.available() )
|
|
IOUtils.copy(inStream,bao)
|
|
return bao.toByteArray()
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// file
|
|
|
|
@Suppress("unused")
|
|
@Throws(IOException::class)
|
|
fun File.loadByteArray() : ByteArray {
|
|
val size = this.length().toInt()
|
|
val data = ByteArray(size)
|
|
FileInputStream(this).use { inStream ->
|
|
val nRead = 0
|
|
while(nRead < size) {
|
|
val delta = inStream.read(data, nRead, size - nRead)
|
|
if(delta <= 0) break
|
|
}
|
|
return data
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
// toast
|
|
|
|
fun showToast(context : Context, bLong : Boolean, fmt : String?, vararg args : Any) {
|
|
Utils.showToastImpl(
|
|
context,
|
|
bLong,
|
|
if(fmt == null) "(null)" else if(args.isEmpty()) fmt else String.format(fmt, *args)
|
|
)
|
|
}
|
|
|
|
fun showToast(context : Context, ex : Throwable, fmt : String?, vararg args : Any) {
|
|
Utils.showToastImpl(context, true, ex.withCaption(fmt, *args))
|
|
}
|
|
|
|
fun showToast(context : Context, bLong : Boolean, string_id : Int, vararg args : Any) {
|
|
Utils.showToastImpl(context, bLong, context.getString(string_id, *args))
|
|
}
|
|
|
|
fun showToast(context : Context, ex : Throwable, string_id : Int, vararg args : Any) {
|
|
Utils.showToastImpl(context, true, ex.withCaption(context.resources, string_id, *args))
|
|
}
|
|
|
|
|
|
fun Cursor.getInt(key:String) =
|
|
getInt(getColumnIndex(key))
|
|
|
|
fun Cursor.getIntOrNull(idx:Int) =
|
|
if(isNull(idx)) null else getInt(idx)
|
|
|
|
fun Cursor.getIntOrNull(key:String) =
|
|
getIntOrNull(getColumnIndex(key))
|
|
|
|
fun Cursor.getLong(key:String) =
|
|
getLong(getColumnIndex(key))
|
|
|
|
//fun Cursor.getLongOrNull(idx:Int) =
|
|
// if(isNull(idx)) null else getLong(idx)
|
|
|
|
//fun Cursor.getLongOrNull(key:String) =
|
|
// getLongOrNull(getColumnIndex(key))
|
|
|
|
fun Cursor.getString(key:String) :String =
|
|
getString(getColumnIndex(key))
|
|
|
|
fun Cursor.getStringOrNull(keyIdx:Int) =
|
|
if(isNull(keyIdx)) null else getString(keyIdx)
|
|
|
|
fun Cursor.getStringOrNull(key:String) =
|
|
getStringOrNull(getColumnIndex(key))
|
|
|
|
|
|
|
|
fun getDocumentName(contentResolver:ContentResolver,uri : Uri) : String {
|
|
val errorName = "no_name"
|
|
return contentResolver.query(uri, null, null, null, null, null)
|
|
?.use { cursor ->
|
|
return if(! cursor.moveToFirst()) {
|
|
errorName
|
|
} else {
|
|
cursor.getStringOrNull(OpenableColumns.DISPLAY_NAME) ?: errorName
|
|
}
|
|
}
|
|
?: errorName
|
|
}
|
|
|
|
@Throws(IOException::class)
|
|
fun getStreamSize(bClose : Boolean, inStream : InputStream) : Long {
|
|
try {
|
|
var size = 0L
|
|
while(true) {
|
|
val r = IOUtils.skip(inStream, 16384)
|
|
if(r <= 0) break
|
|
size += r
|
|
}
|
|
return size
|
|
} finally {
|
|
@Suppress("DEPRECATION")
|
|
if(bClose) IOUtils.closeQuietly(inStream)
|
|
}
|
|
}
|
|
|
|
fun intentOpenDocument(mimeType : String):Intent{
|
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
intent.type = mimeType // "image/*"
|
|
return intent
|
|
}
|
|
|
|
fun intentGetContent(
|
|
allowMultiple : Boolean,
|
|
caption : String,
|
|
vararg mimeTypes : String
|
|
) : Intent {
|
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
|
|
if(allowMultiple) {
|
|
// EXTRA_ALLOW_MULTIPLE は API 18 (4.3)以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる
|
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
|
}
|
|
|
|
// EXTRA_MIME_TYPES は API 19以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる
|
|
intent.putExtra("android.intent.extra.MIME_TYPES", mimeTypes)
|
|
|
|
intent.type =when {
|
|
mimeTypes.size == 1 -> mimeTypes[0]
|
|
|
|
// On Android 6.0 and above using "video/* image/" or "image/ video/*" type doesn't work
|
|
// it only recognizes the first filter you specify.
|
|
Build.VERSION.SDK_INT >= 23 -> "*/*"
|
|
|
|
else -> mimeTypes.joinToString(" ")
|
|
}
|
|
|
|
return Intent.createChooser(intent, caption)
|
|
}
|
|
|
|
// returns list of pair of uri and mime-type.
|
|
fun Intent.handleGetContentResult(contentResolver : ContentResolver) : ArrayList<Pair<Uri, String?>> {
|
|
val urlList = ArrayList<Pair<Uri, String?>>()
|
|
// 単一選択
|
|
this.data?.let {
|
|
urlList.add(Pair(it, this.type))
|
|
}
|
|
// 複数選択
|
|
val cd = this.clipData
|
|
if(cd != null) {
|
|
for(i in 0 until cd.itemCount) {
|
|
cd.getItemAt(i)?.uri?.let { uri ->
|
|
if(null == urlList.find { it.first == uri }) {
|
|
urlList.add(Pair(uri, null as String?))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
urlList.forEach {
|
|
try{
|
|
contentResolver.takePersistableUriPermission(
|
|
it.first,
|
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
)
|
|
}catch(_:Throwable){
|
|
}
|
|
}
|
|
return urlList
|
|
}
|
|
|
|
fun Matcher.groupOrNull( g:Int):String? =
|
|
if( groupCount() >= g ){
|
|
group(g)
|
|
}else {
|
|
null
|
|
}
|
|
|