SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/util/Utils.kt

762 lines
22 KiB
Kotlin

package jp.juggler.subwaytooter.util
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.res.Resources
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
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 me.drakeet.support.toast.ToastCompat
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.lang.ref.WeakReference
import java.security.MessageDigest
import java.util.ArrayList
import java.util.HashMap
import java.util.LinkedList
import java.util.Locale
import java.util.regex.Pattern
import org.apache.commons.io.IOUtils
import org.json.JSONArray
import org.json.JSONObject
object Utils {
val log = LogCategory("Utils")
val hex =
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"
// 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 const val LRE = 0x202A.toChar() // Left-to-right embedding (LRE)
private const val RLE = 0x202B.toChar() // Right-to-left embedding (RLE)
const val PDF = 0x202C.toChar() // Pop directional formatting (PDF)
private const val LRO = 0x202D.toChar() // Left-to-right override (LRO)
private const val RLO = 0x202E.toChar() // Right-to-left override (RLO)
const val CHARS_MUST_PDF = LRE.toString() + RLE + LRO + RLO
private const val LRI = 0x2066.toChar() // Left-to-right isolate (LRI)
private const val RLI = 0x2067.toChar() // Right-to-left isolate (RLI)
private const val FSI = 0x2068.toChar() // First strong isolate (FSI)
const val PDI = 0x2069.toChar() // Pop directional isolate (PDI)
const val CHARS_MUST_PDI = LRI.toString() + RLI + FSI
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 encodeBase64Safe(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()
}
//private fun ByteArray.encodeSHA256() : ByteArray? {
// return try {
// val digest = MessageDigest.getInstance("SHA-256")
// digest.reset()
// digest.digest(this)
// } catch(e1 : NoSuchAlgorithmException) {
// null
// }
//
//}
//
//private fun ByteArray?.encodeBase64Safe() : String? {
// this ?: return null
// return try {
// Base64.encodeToString(this, Base64.URL_SAFE)
// } catch(ex : Throwable) {
// null
// }
//}
////////////////////////////////////////////////////////////////////
// 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
//}
////////////////////////////////////////////////////////////////////
// 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.hex[(value shr 4) and 15])
this.append(Utils.hex[value and 15])
return this
}
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(i in 0 until this.length) {
val c = this[i]
if(- 1 != Utils.CHARS_MUST_PDF.indexOf(c)) {
if(stack == null) stack = LinkedList()
stack.add(Utils.PDF)
} else if(- 1 != Utils.CHARS_MUST_PDI.indexOf(c)) {
if(stack == null) stack = LinkedList()
stack.add(Utils.PDI)
} else if(stack?.isNotEmpty() == true && stack.last == c) {
stack.removeLast()
}
}
if(stack?.isNotEmpty() == true) {
val sb = StringBuilder()
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
//}
// MD5ハッシュの作成
@Suppress("unused")
fun String.digestMD5() : String {
val md = MessageDigest.getInstance("MD5")
md.reset()
return md.digest(this.encodeUTF8()).encodeHex()
}
fun String.digestSHA256() : String {
val md = MessageDigest.getInstance("SHA-256")
md.reset()
return md.digest(this.encodeUTF8()).encodeHex()
}
////////////////////////////////////////////////////////////////////
// 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 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))
fun JSONArray.toStringArrayList() : ArrayList<String> {
val size = this.length()
val dst_list = ArrayList<String>(size)
for(i in 0 until size) {
val sv = this.parseString(i) ?: continue
dst_list.add(sv)
}
return dst_list
}
// 文字列データを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
}
}
////////////////////////////////////////////////////////////////////
// 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() }
}
}
////////////////////////////////////////////////////////////////////
// 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(res_id : Int) : ByteArray? {
try {
this.resources.openRawResource(res_id).use { inStream ->
val bao = ByteArrayOutputStream()
IOUtils.copy(inStream, bao)
return bao.toByteArray()
}
} catch(ex : Throwable) {
Utils.log.trace(ex)
}
return null
}
////////////////////////////////////////////////////////////////////
// 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))
}