fix update access token may notwork.

This commit is contained in:
tateisu 2019-10-06 20:23:33 +09:00
parent 0ebc21a744
commit f42e79be3e
69 changed files with 8316 additions and 8356 deletions

View File

@ -19,8 +19,10 @@
<w>buggie</w>
<w>bumptech</w>
<w>codepoint</w>
<w>coord</w>
<w>coroutine</w>
<w>coroutines</w>
<w>denomi</w>
<w>doctype</w>
<w>dont</w>
<w>emoji</w>

View File

@ -7,7 +7,7 @@
</component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myDefaultNotNull" value="android.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="12">

View File

@ -496,8 +496,8 @@ class ActMain : AppCompatActivity()
bStart = true
log.d("onStart")
var ts= SystemClock.elapsedRealtime()
var te:Long
var ts = SystemClock.elapsedRealtime()
var te : Long
// カラーカスタマイズを読み直す
ListDivider.color = Pref.ipListDividerColor(pref)
@ -512,9 +512,9 @@ class ActMain : AppCompatActivity()
CustomShare.reloadCache(this, pref)
te = SystemClock.elapsedRealtime()
if( te-ts >= 100L) log.w("onStart: ${te-ts}ms : reload color")
if(te - ts >= 100L) log.w("onStart: ${te - ts}ms : reload color")
ts = SystemClock.elapsedRealtime()
var tz = TimeZone.getDefault()
try {
val tz_id = Pref.spTimeZone(pref)
@ -527,9 +527,9 @@ class ActMain : AppCompatActivity()
TootStatus.date_format.timeZone = tz
te = SystemClock.elapsedRealtime()
if( te-ts >= 100L) log.w("onStart: ${te-ts}ms : reload timezone")
if(te - ts >= 100L) log.w("onStart: ${te - ts}ms : reload timezone")
ts = SystemClock.elapsedRealtime()
// バグいアカウントデータを消す
try {
SavedAccount.sweepBuggieData()
@ -538,7 +538,7 @@ class ActMain : AppCompatActivity()
}
te = SystemClock.elapsedRealtime()
if( te-ts >= 100L) log.w("onStart: ${te-ts}ms : sweepBuggieData")
if(te - ts >= 100L) log.w("onStart: ${te - ts}ms : sweepBuggieData")
ts = SystemClock.elapsedRealtime()
// アカウント設定から戻ってきたら、カラムを消す必要があるかもしれない
@ -560,7 +560,7 @@ class ActMain : AppCompatActivity()
}
te = SystemClock.elapsedRealtime()
if( te-ts >= 100L) log.w("onStart: ${te-ts}ms : column order")
if(te - ts >= 100L) log.w("onStart: ${te - ts}ms : column order")
ts = SystemClock.elapsedRealtime()
// 背景画像を表示しない設定が変更された時にカラムの背景を設定しなおす
@ -569,22 +569,21 @@ class ActMain : AppCompatActivity()
}
te = SystemClock.elapsedRealtime()
if( te-ts >= 100L) log.w("onStart: ${te-ts}ms :fireColumnColor")
if(te - ts >= 100L) log.w("onStart: ${te - ts}ms :fireColumnColor")
ts = SystemClock.elapsedRealtime()
// 各カラムのアカウント設定を読み直す
reloadAccountSetting()
te = SystemClock.elapsedRealtime()
if( te-ts >= 100L) log.w("onStart: ${te-ts}ms :reloadAccountSetting")
if(te - ts >= 100L) log.w("onStart: ${te - ts}ms :reloadAccountSetting")
ts = SystemClock.elapsedRealtime()
// 投稿直後ならカラムの再取得を行う
refreshAfterPost()
te = SystemClock.elapsedRealtime()
if( te-ts >= 100L) log.w("onStart: ${te-ts}ms :refreshAfterPost")
if(te - ts >= 100L) log.w("onStart: ${te - ts}ms :refreshAfterPost")
ts = SystemClock.elapsedRealtime()
// 画面復帰時に再取得やストリーミング開始を行う
@ -593,14 +592,14 @@ class ActMain : AppCompatActivity()
}
te = SystemClock.elapsedRealtime()
if( te-ts >= 100L) log.w("onStart: ${te-ts}ms :column.onStart")
if(te - ts >= 100L) log.w("onStart: ${te - ts}ms :column.onStart")
ts = SystemClock.elapsedRealtime()
// カラムの表示範囲インジケータを更新
updateColumnStripSelection(- 1, - 1f)
te = SystemClock.elapsedRealtime()
if( te-ts >= 100L) log.w("onStart: ${te-ts}ms :updateColumnStripSelection")
if(te - ts >= 100L) log.w("onStart: ${te - ts}ms :updateColumnStripSelection")
ts = SystemClock.elapsedRealtime()
@ -609,16 +608,14 @@ class ActMain : AppCompatActivity()
}
te = SystemClock.elapsedRealtime()
if( te-ts >= 100L) log.w("onStart: ${te-ts}ms :fireShowContent")
ts = SystemClock.elapsedRealtime()
if(te - ts >= 100L) log.w("onStart: ${te - ts}ms :fireShowContent")
// 相対時刻表示
proc_updateRelativeTime.run()
te = SystemClock.elapsedRealtime()
if( te-tsTotal >= 100L) log.w("onStart: ${te-tsTotal}ms : total")
if(te - tsTotal >= 100L) log.w("onStart: ${te - tsTotal}ms : total")
}
override fun onStop() {
@ -1768,8 +1765,8 @@ class ActMain : AppCompatActivity()
client.instance = instance
}
val (r2, ti) = TootInstance.get(client)
if(ti == null) return r2
val (ti, r2) = TootInstance.get(client)
ti ?: return r2
this.host = instance
val client_name = Pref.spClientName(this@ActMain)
@ -1881,7 +1878,11 @@ class ActMain : AppCompatActivity()
// cancelled.
}
error != null -> showToast(this@ActMain, true, "${result.error} ${result.requestInfo}".trim() )
error != null -> showToast(
this@ActMain,
true,
"${result.error} ${result.requestInfo}".trim()
)
token_info == null -> showToast(this@ActMain, true, "can't get access token.")
@ -1992,8 +1993,8 @@ class ActMain : AppCompatActivity()
override fun background(client : TootApiClient) : TootApiResult? {
val (instanceResult, instance) = TootInstance.get(client,host)
if(instance == null) return instanceResult
val (instance, instanceResult) = TootInstance.get(client, host)
instance ?: return instanceResult
val misskeyVersion = instance.misskeyVersion
val linkHelper = LinkHelper.newLinkHelper(host, misskeyVersion = misskeyVersion)

View File

@ -97,24 +97,22 @@ class ActPost : AppCompatActivity(),
internal const val MIME_TYPE_JPEG = "image/jpeg"
internal const val MIME_TYPE_PNG = "image/png"
internal val resizeConfigList = arrayOf(
ResizeConfig(ResizeType.None,0),
ResizeConfig(ResizeType.LongSide,640),
ResizeConfig(ResizeType.LongSide,800),
ResizeConfig(ResizeType.LongSide,1024),
ResizeConfig(ResizeType.LongSide,1280),
ResizeConfig(ResizeType.LongSide,1600),
ResizeConfig(ResizeType.LongSide,2048),
ResizeConfig(ResizeType.None, 0),
ResizeConfig(ResizeType.SquarePixel,640),
ResizeConfig(ResizeType.SquarePixel,800),
ResizeConfig(ResizeType.SquarePixel,1024),
ResizeConfig(ResizeType.SquarePixel,1280),
ResizeConfig(ResizeType.SquarePixel,1600),
ResizeConfig(ResizeType.SquarePixel,2048)
ResizeConfig(ResizeType.LongSide, 640),
ResizeConfig(ResizeType.LongSide, 800),
ResizeConfig(ResizeType.LongSide, 1024),
ResizeConfig(ResizeType.LongSide, 1280),
ResizeConfig(ResizeType.LongSide, 1600),
ResizeConfig(ResizeType.LongSide, 2048),
ResizeConfig(ResizeType.SquarePixel, 640),
ResizeConfig(ResizeType.SquarePixel, 800),
ResizeConfig(ResizeType.SquarePixel, 1024),
ResizeConfig(ResizeType.SquarePixel, 1280),
ResizeConfig(ResizeType.SquarePixel, 1600),
ResizeConfig(ResizeType.SquarePixel, 2048)
)
internal val acceptable_mime_types = HashSet<String>().apply {
@ -1309,7 +1307,7 @@ class ActPost : AppCompatActivity(),
var newInfo : TootInstance? = null
override fun background(client : TootApiClient) : TootApiResult? {
val (result, ti) = TootInstance.get(client)
val (ti, result) = TootInstance.get(client)
newInfo = ti
return result
}
@ -2121,8 +2119,8 @@ class ActPost : AppCompatActivity(),
client.account = account
val (tiResult, ti) = TootInstance.get(client)
if(ti == null) return tiResult
val (ti, tiResult) = TootInstance.get(client)
ti ?: return tiResult
if(ti.instanceType == TootInstance.InstanceType.Pixelfed) {
if(in_reply_to_id != null) {

View File

@ -4,21 +4,28 @@ import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
import jp.juggler.util.LogCategory
import java.lang.ref.WeakReference
class ChooseReceiver :BroadcastReceiver(){
class ChooseReceiver : BroadcastReceiver() {
companion object{
var lastComponentName: ComponentName? = null
var refCallback : WeakReference<()->Unit>? = null
companion object {
private val log = LogCategory("ChooseReceiver")
var lastComponentName : ComponentName? = null
var refCallback : WeakReference<() -> Unit>? = null
fun setCallback(cb:()->Unit){
fun setCallback(cb : () -> Unit) {
refCallback = WeakReference(cb)
}
}
override fun onReceive(context: Context,intent: Intent?) {
lastComponentName = intent?.extras?.get(Intent.EXTRA_CHOSEN_COMPONENT) as? ComponentName
refCallback?.get()?.invoke()
override fun onReceive(context : Context, intent : Intent?) {
if(Build.VERSION.SDK_INT >= 22) {
lastComponentName = intent?.extras?.get(Intent.EXTRA_CHOSEN_COMPONENT) as? ComponentName
refCallback?.get()?.invoke()
} else {
log.w("onReceive: Intent.EXTRA_CHOSEN_COMPONENT can't be used in API level 21")
}
}
}

View File

@ -48,9 +48,9 @@ class ColumnTask_Loading(
column.muted_word2 = column.encodeFilterTree(column.loadFilter2(client))
if( !access_info.isNA) {
val (instanceResult, instance) = TootInstance.get(client)
if(instance == null) return instanceResult
if(! access_info.isNA) {
val (instance, instanceResult) = TootInstance.get(client)
instance ?: return instanceResult
if(instance.instanceType == TootInstance.InstanceType.Pixelfed) {
return TootApiResult("currently Pixelfed instance is not supported.")
}
@ -121,7 +121,6 @@ class ColumnTask_Loading(
/////////////////////////////////////////////////////////////////
// functions that called from ColumnTask.loading lambda.
internal fun getStatusesPinned(client : TootApiClient, path_base : String) {
val result = client.request(path_base)
val jsonArray = result?.jsonArray
@ -691,8 +690,8 @@ class ColumnTask_Loading(
) : TootApiResult? {
// (Mastodonのみ対応)
val (instanceResult, instance) = TootInstance.get(client)
if(instance == null) return instanceResult
val (instance, instanceResult) = TootInstance.get(client)
instance ?: return instanceResult
// ステータスIDに該当するトゥート
// タンスをまたいだりすると存在しないかもしれないが、エラーは出さない
@ -735,8 +734,8 @@ class ColumnTask_Loading(
internal fun getAccountAroundStatuses(client : TootApiClient) : TootApiResult? {
// (Mastodonのみ対応)
val (instanceResult, instance) = TootInstance.get(client)
if(instance == null) return instanceResult
val (instance, instanceResult) = TootInstance.get(client)
instance ?: return instanceResult
// ステータスIDに該当するトゥート
// タンスをまたいだりすると存在しないかもしれない
@ -986,8 +985,8 @@ class ColumnTask_Loading(
return TootApiResult(context.getString(R.string.search_is_not_available_on_pseudo_account))
}
val(instanceResult,instance) = TootInstance.get(client)
if( instance==null) return instanceResult
val (instance, instanceResult) = TootInstance.get(client)
instance ?: return instanceResult
var query = "q=${column.search_query.encodePercent()}"
if(column.search_resolve) query += "&resolve=1"

View File

@ -57,10 +57,10 @@ enum class ColumnType(
ProfileStatusMastodon(
loading = { client ->
val(instanceResult,instance) = TootInstance.get(client)
if(instance==null){
val (instance, instanceResult) = TootInstance.get(client)
if(instance == null) {
instanceResult
}else {
} else {
val path = column.makeProfileStatusesUrl(column.profile_id)
if(instance.versionGE(TootInstance.VERSION_1_6)
@ -1095,26 +1095,31 @@ enum class ColumnType(
headerType = HeaderType.Instance,
loading = { client ->
val(instanceResult,instance) = TootInstance.get(client,column.instance_uri,allowPixelfed = true)
if(instance!=null) {
val (instance, instanceResult) = TootInstance.get(
client,
column.instance_uri,
allowPixelfed = true,
forceUpdate = true
)
if(instance != null) {
column.instance_information = instance
column.handshake = instanceResult?.response?.handshake
}
instanceResult
//
// // 「インスタンス情報」カラムをNAアカウントで開く場合
// instance_name != null -> client.instance = instance_name
//
// val (result, ti) = client.parseInstanceInformation(client.getInstanceInformation())
// instance_tmp = ti
// return result
// }
//
// val result = getInstanceInformation(client, column.instance_uri)
// if(instance_tmp != null) {
//
// }
// result
//
// // 「インスタンス情報」カラムをNAアカウントで開く場合
// instance_name != null -> client.instance = instance_name
//
// val (result, ti) = client.parseInstanceInformation(client.getInstanceInformation())
// instance_tmp = ti
// return result
// }
//
// val result = getInstanceInformation(client, column.instance_uri)
// if(instance_tmp != null) {
//
// }
// result
}
),
@ -1342,7 +1347,7 @@ enum class ColumnType(
PROFILE_DIRECTORY(36,
iconId = { R.drawable.ic_follow_plus },
name1 = { it.getString(R.string.profile_directory) },
name2 = { context.getString(R.string.profile_directory_of, instance_uri)},
name2 = { context.getString(R.string.profile_directory_of, instance_uri) },
bAllowPseudo = true,
headerType = HeaderType.ProfileDirectory,
loading = { client ->
@ -1358,8 +1363,8 @@ enum class ColumnType(
iconId = { R.drawable.ic_account_box },
name1 = { it.getString(R.string.account_tl_around) },
name2 = {
val id =status_id?.toString() ?: "null"
context.getString(R.string.account_tl_around_of,id)
val id = status_id?.toString() ?: "null"
context.getString(R.string.account_tl_around_of, id)
},
loading = { client -> getAccountAroundStatuses(client) },

View File

@ -1089,7 +1089,7 @@ class PollingWorker private constructor(contextArg : Context) {
if(wps.flags != 0) {
job.bPollingRequired.set(true)
val (instanceResult, instance) = TootInstance.get(client)
val (instance, instanceResult) = TootInstance.get(client)
if(instance == null) {
if(instanceResult != null) log.e("${instanceResult.error} ${instanceResult.requestInfo}".trim())
return

View File

@ -1,10 +1,10 @@
package jp.juggler.subwaytooter.action
import android.content.Context
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.Pref
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.TootRelationShip
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.util.LogCategory
@ -32,19 +32,21 @@ internal fun addPseudoAccount(
}
if(instanceInfo == null) {
TootTaskRunner(context).run(host,object : TootTask {
TootTaskRunner(context).run(host, object : TootTask {
var targetInstance :TootInstance? =null
var targetInstance : TootInstance? = null
override fun background(client : TootApiClient) : TootApiResult? {
val(instanceResult,instance) = TootInstance.get(client)
if(instance!=null) targetInstance = instance
val (instance, instanceResult) = TootInstance.get(client)
targetInstance = instance
return instanceResult
}
override fun handleResult(result : TootApiResult?) =when{
result==null ->{}
targetInstance == null -> showToast(context,false,result.error)
override fun handleResult(result : TootApiResult?) = when {
result == null -> {
}
targetInstance == null -> showToast(context, false, result.error)
else -> addPseudoAccount(context, host, targetInstance, callback)
}
})
@ -62,7 +64,7 @@ internal fun addPseudoAccount(
JSONObject(),
misskeyVersion = instanceInfo.misskeyVersion
)
account = SavedAccount.loadAccount(context, row_id)
if(account == null) {
throw RuntimeException("loadAccount returns null.")
@ -122,24 +124,24 @@ internal fun saveUserRelationMisskey(
return relation
}
// relationshipを取得
internal fun loadRelation1Mastodon(
client : TootApiClient,
access_info : SavedAccount,
who : TootAccount
) : RelationResult {
val rr = RelationResult()
rr.result = client.request("/api/v1/accounts/relationships?id=${who.id}")
val r2 = rr.result
val jsonArray = r2?.jsonArray
if(jsonArray != null) {
val list = parseList(::TootRelationShip, TootParser(client.context, access_info), jsonArray)
if(list.isNotEmpty()) {
rr.relation = saveUserRelation(access_info, list[0])
}
}
return rr
}
//// relationshipを取得
//internal fun loadRelation1Mastodon(
// client : TootApiClient,
// access_info : SavedAccount,
// who : TootAccount
//) : RelationResult {
// val rr = RelationResult()
// rr.result = client.request("/api/v1/accounts/relationships?id=${who.id}")
// val r2 = rr.result
// val jsonArray = r2?.jsonArray
// if(jsonArray != null) {
// val list = parseList(::TootRelationShip, TootParser(client.context, access_info), jsonArray)
// if(list.isNotEmpty()) {
// rr.relation = saveUserRelation(access_info, list[0])
// }
// }
// return rr
//}
// 別アカ操作と別タンスの関係
const val NOT_CROSS_ACCOUNT = 1

View File

@ -36,7 +36,7 @@ object Action_Account {
LoginForm.Action.Create -> client.createUser1(Pref.spClientName(activity))
LoginForm.Action.Pseudo, LoginForm.Action.Token -> {
val (ri, ti) = TootInstance.get(client)
val (ti, ri) = TootInstance.get(client)
if(ti != null) ri?.data = ti
ri
}

View File

@ -25,7 +25,7 @@ object Action_Instance {
instance == null -> TootTaskRunner(activity).run(host, object : TootTask {
var targetInstance : TootInstance? = null
override fun background(client : TootApiClient) : TootApiResult? {
val (ri, ti) = TootInstance.get(client, host, allowPixelfed = true)
val (ti, ri) = TootInstance.get(client, host, allowPixelfed = true)
targetInstance = ti
return ri
}

View File

@ -1039,13 +1039,11 @@ class TootApiClient(
forceUpdateClient : Boolean = false
) : TootApiResult? {
val (ri, ti) = TootInstance.get(this)
return if(ti == null || (ri?.response?.code ?: 0) !in 200 until 300) {
ri
} else if(ti.misskeyVersion > 0) {
authentication1Misskey(clientNameArg, ti)
} else {
authentication1Mastodon(clientNameArg, ti, forceUpdateClient)
val (ti, ri) = TootInstance.get(this)
ti ?: return ri
return when {
ti.misskeyVersion > 0 -> authentication1Misskey(clientNameArg, ti)
else -> authentication1Mastodon(clientNameArg, ti, forceUpdateClient)
}
}
@ -1150,8 +1148,9 @@ class TootApiClient(
fun createUser1(clientNameArg : String) : TootApiResult? {
val (ri, ti) = TootInstance.get(this)
if(ti == null) return ri
val (ti, ri) = TootInstance.get(this)
ti ?: return ri
return when(ti.instanceType) {
TootInstance.InstanceType.Misskey ->
TootApiResult("Misskey has no API to create new account")
@ -1185,15 +1184,15 @@ class TootApiClient(
if(! sendRequest(result) {
val params = ArrayList<String>().apply{
val params = ArrayList<String>().apply {
add("username=${username.encodePercent()}")
add("email=${email.encodePercent()}")
add("password=${password.encodePercent()}")
add("agreement=${agreement}")
if( reason?.isNotEmpty() == true ) add("reason=${reason.encodePercent()}")
if(reason?.isNotEmpty() == true) add("reason=${reason.encodePercent()}")
}
params
.joinToString("&").toFormRequestBody().toPost()
.url("https://$instance/api/v1/accounts")

View File

@ -269,8 +269,9 @@ class TootInstance(parser : TootParser, src : JSONObject) {
client : TootApiClient,
host : String? = client.instance,
account : SavedAccount? = if(host == client.instance) client.account else null,
allowPixelfed : Boolean = false
) : Pair<TootApiResult?, TootInstance?> {
allowPixelfed : Boolean = false,
forceUpdate :Boolean = false
) : Pair<TootInstance?,TootApiResult?> {
val tmpInstance = client.instance
val tmpAccount = client.account
@ -282,23 +283,27 @@ class TootInstance(parser : TootParser, src : JSONObject) {
// ホスト名ごとに用意したオブジェクトで同期する
val cacheEntry = getCacheEntry(instanceName)
synchronized(cacheEntry) {
// re-use cached item.
val now = SystemClock.elapsedRealtime()
var item = cacheEntry.data
if(item != null && now - item.time_parse <= EXPIRE) {
if(item.instanceType == InstanceType.Pixelfed &&
! Pref.bpEnablePixelfed(App1.pref) &&
! allowPixelfed
) {
return Pair(
TootApiResult("currently Pixelfed instance is not supported."),
null
)
var item: TootInstance?
if(!forceUpdate) {
// re-use cached item.
val now = SystemClock.elapsedRealtime()
item = cacheEntry.data
if(item != null && now - item.time_parse <= EXPIRE) {
if(item.instanceType == InstanceType.Pixelfed &&
! Pref.bpEnablePixelfed(App1.pref) &&
! allowPixelfed
) {
return Pair(
null,
TootApiResult("currently Pixelfed instance is not supported.")
)
}
return Pair(item,TootApiResult() )
}
return Pair(TootApiResult(), item)
}
// get new information
@ -315,7 +320,7 @@ class TootInstance(parser : TootParser, src : JSONObject) {
client.getInstanceInformation()
}
val json = result?.jsonObject ?: return Pair(result, null)
val json = result?.jsonObject ?: return Pair(null,result)
item = parseItem(
::TootInstance,
@ -335,19 +340,22 @@ class TootInstance(parser : TootParser, src : JSONObject) {
return when {
item == null ->
Pair(result.setError("can't parse data in instance information."), null)
Pair(
null,
result.setError("instance information parse error.")
)
item.instanceType == InstanceType.Pixelfed &&
! Pref.bpEnablePixelfed(App1.pref) &&
! allowPixelfed ->
Pair(
result.setError("currently Pixelfed instance is not supported."),
null
null,
result.setError("currently Pixelfed instance is not supported.")
)
else -> {
cacheEntry.data = item
Pair(result, item)
Pair(item,result)
}
}
}

View File

@ -127,7 +127,8 @@ object LoginForm {
activity.resources.openRawResource(R.raw.server_list).use { inStream ->
val br = BufferedReader(InputStreamReader(inStream, "UTF-8"))
while(true) {
val s : String = br.readLine()?.trim { it <= ' ' }?.toLowerCase(Locale.JAPAN) ?: break
val s : String =
br.readLine()?.trim { it <= ' ' }?.toLowerCase(Locale.JAPAN) ?: break
if(s.isNotEmpty()) instance_list.add(s)
}
}
@ -145,10 +146,10 @@ object LoginForm {
return value as String
}
override fun performFiltering(constraint : CharSequence?) : Filter.FilterResults {
val result = Filter.FilterResults()
override fun performFiltering(constraint : CharSequence?) : FilterResults {
val result = FilterResults()
if(constraint?.isNotEmpty() == true) {
val key = constraint.toString().toLowerCase()
val key = constraint.toString().toLowerCase(Locale.JAPAN)
// suggestions リストは毎回生成する必要がある。publishResultsと同時にアクセスされる場合がある
val suggestions = StringArray()
for(s in instance_list) {
@ -165,7 +166,7 @@ object LoginForm {
override fun publishResults(
constraint : CharSequence?,
results : Filter.FilterResults?
results : FilterResults?
) {
clear()
val values = results?.values

View File

@ -350,8 +350,8 @@ class PostHelper(
var visibility_checked : TootVisibility? = visibility
val (ri, instance) = TootInstance.get(client)
if(instance == null) return ri
val (instance, ri) = TootInstance.get(client)
instance ?: return ri
if(instance.instanceType == TootInstance.InstanceType.Pixelfed) {
if(in_reply_to_id != null && attachment_list?.isNotEmpty() == true) {

View File

@ -89,15 +89,15 @@ class PushSubscriptionHelper(
.toPostRequestBuilder()
.url("${PollingWorker.APP_SERVER}/webpushserverkey")
.build()
)
val res = r?.response
when(res?.code) {
null ->{
null -> {
}
200 -> {
// 登録できたサーバーキーをアプリ内DBに保存
SubscriptionServerKey.save(clientIdentifier, serverKey)
@ -188,7 +188,7 @@ class PushSubscriptionHelper(
var subscription404 = false
when(res.code) {
200 -> {
if( r.error?.isNotEmpty() == true && r.jsonObject == null ){
if(r.error?.isNotEmpty() == true && r.jsonObject == null) {
// Pleromaが200応用でエラーHTMLを返す
return TootApiResult(
error = context.getString(
@ -196,7 +196,7 @@ class PushSubscriptionHelper(
)
)
}
// たぶん購読が存在する
}
@ -215,7 +215,7 @@ class PushSubscriptionHelper(
}
}
in 400 until 500 ->{
in 400 until 500 -> {
return TootApiResult(
error = context.getString(
R.string.instance_does_not_support_push_api_pleroma
@ -234,9 +234,9 @@ class PushSubscriptionHelper(
if(oldSubscription == null) {
// 現在の購読状況が分からない場合はインスタンスのバージョンを調べる必要がある
val(result,ti) = TootInstance.get(client)
val (ti, result) = TootInstance.get(client)
ti ?: return result
if(! ti.versionGE(TootInstance.VERSION_2_4_0_rc1)) {
// 2.4.0rc1 未満にはプッシュ購読APIはない
return TootApiResult(
@ -255,6 +255,7 @@ class PushSubscriptionHelper(
if(verbose) addLog(context.getString(R.string.push_subscription_not_exists))
return TootApiResult()
}
else -> {
// 2.4.0rc1では「APIが存在しない」と「購読が存在しない」を判別できない
}
@ -296,7 +297,7 @@ class PushSubscriptionHelper(
r = client.http(
JSONObject()
.put("token_digest", tokenDigest)
.put("install_id", install_id)
.put("install_id", install_id)
.toPostRequestBuilder()
.url("${PollingWorker.APP_SERVER}/webpushtokencheck")
.build()

View File

@ -7,9 +7,9 @@ import android.graphics.Rect
import android.graphics.Typeface
import android.util.AttributeSet
import android.view.ViewGroup
import android.widget.ImageButton
import androidx.appcompat.widget.AppCompatImageButton
class CountImageButton : ImageButton{
class CountImageButton : AppCompatImageButton {
constructor(context : Context) : super(context) {
init(context)
@ -44,11 +44,11 @@ class CountImageButton : ImageButton{
invalidate()
}
var compoundPadding = 0
var paddingRightOriginal = 0
var paddingRightWithText = 0
val textBounds = Rect()
var textWidth = 0
private var compoundPadding = 0
private var paddingRightOriginal = 0
private var paddingRightWithText = 0
private val textBounds = Rect()
private var textWidth = 0
override fun setPadding(left : Int, top : Int, right : Int, bottom : Int) {
paddingRightOriginal = right

View File

@ -3,114 +3,110 @@ package jp.juggler.util
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.storage.StorageManager
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import org.apache.commons.io.IOUtils
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.util.*
internal object StorageUtils{
private val log = LogCategory("StorageUtils")
private const val PATH_TREE = "tree"
private const val PATH_DOCUMENT = "document"
internal class FileInfo(any_uri : String?) {
var uri : Uri? = null
private var mime_type : String? = null
init {
if(any_uri != null) {
uri = if(any_uri.startsWith("/")) {
Uri.fromFile(File(any_uri))
} else {
any_uri.toUri()
}
val ext = MimeTypeMap.getFileExtensionFromUrl(any_uri)
if(ext != null) {
mime_type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase())
}
}
}
}
private fun getSecondaryStorageVolumesMap(context : Context) : Map<String, String> {
val result = HashMap<String, String>()
try {
val sm = context.applicationContext.getSystemService(Context.STORAGE_SERVICE) as? StorageManager
if(sm == null) {
log.e("can't get StorageManager")
} else {
// SDカードスロットのある7.0端末が手元にないから検証できない
// if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ){
// for(StorageVolume volume : sm.getStorageVolumes() ){
// // String path = volume.getPath();
// String state = volume.getState();
//
// }
// }
val getVolumeList = sm.javaClass.getMethod("getVolumeList")
val volumes = getVolumeList.invoke(sm)
log.d("volumes type=%s", volumes.javaClass)
if(volumes is ArrayList<*>) {
//
for(volume in volumes) {
val volume_clazz = volume.javaClass
val path = volume_clazz.getMethod("getPath").invoke(volume) as? String
val state = volume_clazz.getMethod("getState").invoke(volume) as? String
if(path != null && state == "mounted") {
//
val isPrimary = volume_clazz.getMethod("isPrimary").invoke(volume) as? Boolean
if(isPrimary == true) result["primary"] = path
//
val uuid = volume_clazz.getMethod("getUuid").invoke(volume) as? String
if(uuid != null) result[uuid] = path
}
}
}
}
} catch(ex : Throwable) {
log.trace(ex)
}
return result
}
private fun isExternalStorageDocument(uri : Uri) : Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
private fun getDocumentId(documentUri : Uri) : String {
val paths = documentUri.pathSegments
if(paths.size >= 2 && PATH_DOCUMENT == paths[0]) {
// document
return paths[1]
}
if(paths.size >= 4 && PATH_TREE == paths[0]
&& PATH_DOCUMENT == paths[2]) {
// document in tree
return paths[3]
}
if(paths.size >= 2 && PATH_TREE == paths[0]) {
// tree
return paths[1]
}
throw IllegalArgumentException("Invalid URI: $documentUri")
}
// internal object StorageUtils{
//
// private val log = LogCategory("StorageUtils")
//
// private const val PATH_TREE = "tree"
// private const val PATH_DOCUMENT = "document"
//
// internal class FileInfo(any_uri : String?) {
//
// var uri : Uri? = null
// private var mime_type : String? = null
//
// init {
// if(any_uri != null) {
// uri = if(any_uri.startsWith("/")) {
// Uri.fromFile(File(any_uri))
// } else {
// any_uri.toUri()
// }
// val ext = MimeTypeMap.getFileExtensionFromUrl(any_uri)
// if(ext != null) {
// mime_type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase(Locale.JAPAN))
// }
// }
// }
// }
//
// private fun getSecondaryStorageVolumesMap(context : Context) : Map<String, String> {
// val result = HashMap<String, String>()
// try {
// val sm = context.applicationContext.getSystemService(Context.STORAGE_SERVICE) as? StorageManager
// if(sm == null) {
// log.e("can't get StorageManager")
// } else {
//
// // SDカードスロットのある7.0端末が手元にないから検証できない
// // if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ){
// // for(StorageVolume volume : sm.getStorageVolumes() ){
// // // String path = volume.getPath();
// // String state = volume.getState();
// //
// // }
// // }
//
// val getVolumeList = sm.javaClass.getMethod("getVolumeList")
// val volumes = getVolumeList.invoke(sm)
// log.d("volumes type=%s", volumes.javaClass)
//
// if(volumes is ArrayList<*>) {
// //
// for(volume in volumes) {
// val volume_clazz = volume.javaClass
//
// val path = volume_clazz.getMethod("getPath").invoke(volume) as? String
// val state = volume_clazz.getMethod("getState").invoke(volume) as? String
// if(path != null && state == "mounted") {
// //
// val isPrimary = volume_clazz.getMethod("isPrimary").invoke(volume) as? Boolean
// if(isPrimary == true) result["primary"] = path
// //
// val uuid = volume_clazz.getMethod("getUuid").invoke(volume) as? String
// if(uuid != null) result[uuid] = path
// }
// }
// }
// }
// } catch(ex : Throwable) {
// log.trace(ex)
// }
//
// return result
// }
//
// private fun isExternalStorageDocument(uri : Uri) : Boolean {
// return "com.android.externalstorage.documents" == uri.authority
// }
//
// private fun getDocumentId(documentUri : Uri) : String {
// val paths = documentUri.pathSegments
// if(paths.size >= 2 && PATH_DOCUMENT == paths[0]) {
// // document
// return paths[1]
// }
// if(paths.size >= 4 && PATH_TREE == paths[0]
// && PATH_DOCUMENT == paths[2]) {
// // document in tree
// return paths[3]
// }
// if(paths.size >= 2 && PATH_TREE == paths[0]) {
// // tree
// return paths[1]
// }
// throw IllegalArgumentException("Invalid URI: $documentUri")
// }
// fun getFile(context : Context, path : String) : File? {
// try {
// if(path.startsWith("/")) return File(path)
@ -163,20 +159,23 @@ internal object StorageUtils{
//
// return null
// }
internal 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
}
const val MIME_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream"
//
//
//
//}
private const val MIME_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream"
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")
@ -190,7 +189,7 @@ fun getMimeType(log : LogCategory?, src : String) : String {
if(mime_type?.isNotEmpty() == true) return mime_type
//
mime_type = StorageUtils.mimeTypeExMap[ext]
mime_type = mimeTypeExMap[ext]
if(mime_type?.isNotEmpty() == true) return mime_type
// 戻り値が空文字列の場合とnullの場合があり、空文字列の場合は既知なのでログ出力しない
@ -199,10 +198,9 @@ fun getMimeType(log : LogCategory?, src : String) : String {
log.w("getMimeType(): unknown file extension '%s'", ext)
}
}
return StorageUtils.MIME_TYPE_APPLICATION_OCTET_STREAM
return MIME_TYPE_APPLICATION_OCTET_STREAM
}
fun getDocumentName(contentResolver : ContentResolver, uri : Uri) : String {
val errorName = "no_name"
return contentResolver.query(uri, null, null, null, null, null)
@ -288,7 +286,6 @@ fun intentGetContent(
return Intent.createChooser(intent, caption)
}
data class GetContentResultEntry(
val uri : Uri,
val mimeType : String? = null,

View File

@ -726,6 +726,7 @@
<TextView
style="@style/setting_row_label"
android:text="@string/max_toot_chars"
android:labelFor="@+id/etMaxTootChars"
/>
<LinearLayout style="@style/setting_row_form">

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/svContent"
android:layout_width="match_parent"
@ -8,24 +7,20 @@
android:clipToPadding="false"
android:fillViewport="true"
android:paddingBottom="12dp"
android:paddingTop="12dp"
android:scrollbarStyle="outsideOverlay"
>
android:paddingBottom="12dp"
android:scrollbarStyle="outsideOverlay">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
android:orientation="vertical">
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
<TextView
style="@style/setting_row_label"
android:text="@string/column_header"
/>
android:text="@string/column_header" />
<LinearLayout style="@style/setting_row_form">
@ -35,11 +30,11 @@
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:gravity="center_vertical"
android:paddingBottom="3dp"
android:paddingEnd="12dp"
android:paddingStart="12dp"
android:paddingTop="3dp"
>
android:paddingEnd="12dp"
android:paddingBottom="3dp"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/ivColumnHeader"
@ -47,25 +42,21 @@
android:layout_height="32dp"
android:layout_marginEnd="4dp"
android:importantForAccessibility="no"
tools:src="?attr/btn_federate_tl"
/>
tools:src="@drawable/ic_bike" />
<TextView
android:id="@+id/tvColumnName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
tools:text="@string/federate_timeline"
/>
tools:text="@string/federate_timeline" />
</LinearLayout>
</LinearLayout>
<TextView
style="@style/setting_row_label_indent1"
android:text="@string/background_color"
/>
<TextView
style="@style/setting_row_label_indent1"
android:text="@string/background_color" />
<LinearLayout style="@style/setting_row_form">
@ -73,23 +64,19 @@
android:id="@+id/btnHeaderBackgroundEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
/>
android:text="@string/edit" />
<Button
android:id="@+id/btnHeaderBackgroundReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
/>
android:text="@string/reset" />
</LinearLayout>
<TextView
style="@style/setting_row_label_indent1"
android:text="@string/foreground_color"
/>
<TextView
style="@style/setting_row_label_indent1"
android:text="@string/foreground_color" />
<LinearLayout style="@style/setting_row_form">
@ -98,70 +85,63 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
<Button
android:id="@+id/btnHeaderTextReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
</LinearLayout>
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
<TextView
style="@style/setting_row_label"
android:text="@string/column"
/>
android:text="@string/column" />
<LinearLayout style="@style/setting_row_form">
<FrameLayout
android:id="@+id/flColumnBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
android:layout_height="wrap_content">
<ImageView
android:id="@+id/ivColumnBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:scaleType="centerCrop"
/>
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:orientation="vertical"
>
android:padding="12dp">
<TextView
android:id="@+id/tvSampleAcct"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="start"
android:maxLines="1"
android:textColor="?attr/colorTimeSmall"
android:textSize="12sp"
android:text="@string/acct_sample"
android:id="@+id/tvSampleAcct"
/>
android:textColor="?attr/colorTimeSmall"
android:textSize="12sp" />
<jp.juggler.subwaytooter.view.MyTextView
android:id="@+id/tvSampleContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:lineSpacingMultiplier="1.1"
android:textColor="?attr/colorContentText"
android:gravity="start"
android:lineSpacingMultiplier="1.1"
android:text="@string/content_sample"
android:id="@+id/tvSampleContent"
/>
android:textColor="?attr/colorContentText" />
</LinearLayout>
</FrameLayout>
@ -171,8 +151,7 @@
<TextView
style="@style/setting_row_label"
android:text="@string/background_color"
/>
android:text="@string/background_color" />
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
@ -182,17 +161,14 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
<Button
android:id="@+id/btnColumnBackgroundColorReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
</LinearLayout>
@ -200,8 +176,7 @@
<TextView
style="@style/setting_row_label"
android:text="@string/background_image"
/>
android:text="@string/background_image" />
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
@ -211,16 +186,14 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pick_image"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
<Button
android:id="@+id/btnColumnBackgroundImageReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
</LinearLayout>
@ -229,21 +202,18 @@
<TextView
style="@style/setting_row_label"
android:labelFor="@+id/etAlpha"
android:text="@string/background_image_alpha"
/>
android:text="@string/background_image_alpha" />
</LinearLayout>
<LinearLayout
style="@style/setting_row_form"
android:layout_height="48dp"
android:baselineAligned="false"
android:gravity="center_vertical"
>
android:gravity="center_vertical">
<View
android:layout_width="0dp"
android:layout_height="48dp"
/>
android:layout_height="48dp" />
<EditText
android:id="@+id/etAlpha"
@ -251,28 +221,25 @@
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:digits="0123456789.,"
android:importantForAutofill="no"
android:inputType="numberDecimal"
android:maxLines="1"
android:minLines="1"
android:minWidth="64dp"
/>
android:minLines="1" />
<SeekBar
android:id="@+id/sbColumnBackgroundAlpha"
style="@style/setting_horizontal_stretch"
android:layout_height="48dp"
android:paddingEnd="32dp"
android:paddingStart="32dp"
/>
android:paddingEnd="32dp" />
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<TextView
style="@style/setting_row_label"
android:text="@string/acct_color"
/>
android:text="@string/acct_color" />
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
@ -282,27 +249,22 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
<Button
android:id="@+id/btnAcctColorReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<TextView
style="@style/setting_row_label"
android:text="@string/content_color"
/>
android:text="@string/content_color" />
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
@ -312,19 +274,17 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
<Button
android:id="@+id/btnContentColorReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
</LinearLayout>
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
</LinearLayout>
</ScrollView>

View File

@ -1,12 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/svContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
@ -15,42 +13,38 @@
android:clipToPadding="false"
android:fillViewport="true"
android:paddingBottom="128dp"
android:paddingTop="12dp"
android:paddingBottom="128dp"
android:scrollbarStyle="outsideOverlay"
tools:ignore="TooManyViews"
>
tools:ignore="TooManyViews">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
android:orientation="vertical">
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
<TextView
style="@style/setting_row_label"
android:labelFor="@+id/etPhrase"
android:text="@string/filter_phrase"
/>
android:text="@string/filter_phrase" />
<LinearLayout style="@style/setting_row_form">
<EditText
android:id="@+id/etPhrase"
style="@style/setting_horizontal_stretch"
android:inputType="text"
/>
android:importantForAutofill="no"
android:inputType="text" />
</LinearLayout>
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
<TextView
style="@style/setting_row_label"
android:text="@string/filter_context"
/>
android:text="@string/filter_context" />
<LinearLayout style="@style/setting_row_form">
@ -58,8 +52,7 @@
android:id="@+id/cbContextHome"
style="@style/setting_horizontal_stretch"
android:checked="true"
android:text="@string/filter_home"
/>
android:text="@string/filter_home" />
</LinearLayout>
@ -69,8 +62,7 @@
android:id="@+id/cbContextNotification"
style="@style/setting_horizontal_stretch"
android:checked="true"
android:text="@string/filter_notification"
/>
android:text="@string/filter_notification" />
</LinearLayout>
@ -80,8 +72,7 @@
android:id="@+id/cbContextPublic"
style="@style/setting_horizontal_stretch"
android:checked="true"
android:text="@string/filter_public"
/>
android:text="@string/filter_public" />
</LinearLayout>
@ -91,60 +82,55 @@
android:id="@+id/cbContextThread"
style="@style/setting_horizontal_stretch"
android:checked="true"
android:text="@string/filter_thread"
/>
android:text="@string/filter_thread" />
</LinearLayout>
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
<TextView
style="@style/setting_row_label"
android:text="@string/filter_options"
/>
android:text="@string/filter_options" />
<LinearLayout style="@style/setting_row_form">
<CheckBox
android:id="@+id/cbFilterIrreversible"
style="@style/setting_horizontal_stretch"
android:text="@string/filter_irreversible_long"
/>
android:text="@string/filter_irreversible_long" />
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<CheckBox
android:id="@+id/cbFilterWordMatch"
style="@style/setting_horizontal_stretch"
android:text="@string/filter_word_match_long"
/>
android:text="@string/filter_word_match_long" />
</LinearLayout>
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
<TextView
style="@style/setting_row_label"
android:text="@string/filter_expires_at"
/>
android:text="@string/filter_expires_at" />
<LinearLayout style="@style/setting_row_form">
<TextView
android:id="@+id/tvExpire"
style="@style/setting_horizontal_stretch"
/>
style="@style/setting_horizontal_stretch" />
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<Spinner
android:id="@+id/spExpire"
style="@style/setting_horizontal_stretch"
/>
style="@style/setting_horizontal_stretch" />
</LinearLayout>
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
<LinearLayout style="@style/setting_row_form">
@ -157,6 +143,5 @@
android:id="@+id/btnSave"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/save"
/>
android:text="@string/save" />
</LinearLayout>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/llContent"
android:layout_width="match_parent"
@ -14,81 +13,72 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:paddingEnd="12dp"
android:paddingStart="12dp"
android:text="@string/preview"
/>
android:paddingEnd="12dp"
android:text="@string/preview" />
<TextView
android:id="@+id/tvPreview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingBottom="6dp"
android:paddingEnd="12dp"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:paddingEnd="12dp"
android:paddingBottom="6dp"
android:textSize="20sp"
tools:text="preview..."
/>
tools:text="preview..." />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fadeScrollbars="false"
android:fillViewport="true"
>
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
>
android:padding="12dp">
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
<TextView
style="@style/setting_row_label"
android:text="@string/acct"
/>
android:text="@string/acct" />
<LinearLayout style="@style/setting_row_form">
<TextView
android:id="@+id/tvAcct"
style="@style/setting_horizontal_stretch"
/>
style="@style/setting_horizontal_stretch" />
</LinearLayout>
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
<TextView
style="@style/setting_row_label"
android:labelFor="@+id/etNickname"
android:text="@string/nickname"
/>
android:text="@string/nickname" />
<LinearLayout style="@style/setting_row_form">
<EditText
android:id="@+id/etNickname"
style="@style/setting_horizontal_stretch"
android:importantForAutofill="no"
android:inputType="text"
android:maxLines="1"
/>
android:maxLines="1" />
</LinearLayout>
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
<TextView
style="@style/setting_row_label"
android:text="@string/text_color"
/>
android:text="@string/text_color" />
<LinearLayout style="@style/setting_row_form">
@ -97,25 +87,22 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
<Button
android:id="@+id/btnTextColorReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
</LinearLayout>
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
<TextView
style="@style/setting_row_label"
android:text="@string/background_color"
/>
android:text="@string/background_color" />
<LinearLayout style="@style/setting_row_form">
@ -124,32 +111,28 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
<Button
android:id="@+id/btnBackgroundColorReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
</LinearLayout>
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
<LinearLayout
android:id="@+id/llNotificationSound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:id="@+id/llNotificationSound"
>
android:orientation="vertical">
<TextView
style="@style/setting_row_label"
android:text="@string/notification_sound_before_oreo"
/>
android:text="@string/notification_sound_before_oreo" />
<LinearLayout style="@style/setting_row_form">
@ -158,20 +141,18 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
<Button
android:id="@+id/btnNotificationSoundReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
</LinearLayout>
<View style="@style/setting_divider"/>
<View style="@style/setting_divider" />
</LinearLayout>
</LinearLayout>
</ScrollView>
@ -181,15 +162,13 @@
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/nickname_applied_after_reload"
android:textSize="12sp"
/>
android:textSize="12sp" />
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:measureWithLargestChild="true"
>
android:measureWithLargestChild="true">
<Button
android:id="@+id/btnSave"
@ -198,8 +177,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/save"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
<Button
android:id="@+id/btnDiscard"
@ -208,7 +186,6 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/discard"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
</LinearLayout>
</LinearLayout>

View File

@ -1,12 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/viewRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
android:orientation="vertical">
<ScrollView
android:id="@+id/scrollView"
@ -18,8 +16,7 @@
android:fadingEdgeLength="20dp"
android:fillViewport="true"
android:requiresFadingEdge="vertical"
android:scrollbarStyle="outsideOverlay"
>
android:scrollbarStyle="outsideOverlay">
<LinearLayout
android:id="@+id/llContent"
@ -29,8 +26,7 @@
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp"
android:paddingBottom="320dp"
>
android:paddingBottom="320dp">
<LinearLayout
android:id="@+id/llReply"
@ -39,21 +35,18 @@
android:layout_marginBottom="4dp"
android:background="?attr/colorReplyBackground"
android:orientation="vertical"
android:padding="6dp"
>
android:padding="6dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/reply_to_this_status"
/>
android:text="@string/reply_to_this_status" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
>
android:orientation="horizontal">
<jp.juggler.subwaytooter.view.MyNetworkImageView
android:id="@+id/ivReply"
@ -61,16 +54,14 @@
android:layout_height="40dp"
android:layout_marginEnd="8dp"
android:background="@drawable/btn_bg_transparent"
android:scaleType="fitCenter"
/>
android:scaleType="fitCenter" />
<TextView
android:id="@+id/tvReplyTo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
/>
android:gravity="center_vertical" />
<ImageButton
android:id="@+id/btnRemoveReply"
@ -81,31 +72,27 @@
android:contentDescription="@string/delete"
android:gravity="center_vertical"
android:src="@drawable/ic_close"
android:tint="?attr/colorVectorDrawable"
/>
android:tint="?attr/colorVectorDrawable" />
</LinearLayout>
<CheckBox
android:id="@+id/cbQuoteRenote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/make_quote_renote"
/>
android:text="@string/make_quote_renote" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@string/post_from"
/>
android:text="@string/post_from" />
<Button
android:id="@+id/btnAccount"
@ -116,8 +103,7 @@
android:gravity="center_vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
</LinearLayout>
<LinearLayout
@ -127,16 +113,14 @@
android:layout_marginTop="4dp"
android:baselineAligned="false"
android:gravity="center_vertical"
android:orientation="horizontal"
>
android:orientation="horizontal">
<jp.juggler.subwaytooter.view.MyNetworkImageView
android:id="@+id/ivMedia1"
android:layout_width="48dp"
android:layout_height="match_parent"
android:background="@drawable/btn_bg_transparent"
android:scaleType="fitCenter"
/>
android:scaleType="fitCenter" />
<jp.juggler.subwaytooter.view.MyNetworkImageView
android:id="@+id/ivMedia2"
@ -144,8 +128,7 @@
android:layout_height="match_parent"
android:layout_marginStart="4dp"
android:background="@drawable/btn_bg_transparent"
android:scaleType="fitCenter"
/>
android:scaleType="fitCenter" />
<jp.juggler.subwaytooter.view.MyNetworkImageView
android:id="@+id/ivMedia3"
@ -153,8 +136,7 @@
android:layout_height="match_parent"
android:layout_marginStart="4dp"
android:background="@drawable/btn_bg_transparent"
android:scaleType="fitCenter"
/>
android:scaleType="fitCenter" />
<jp.juggler.subwaytooter.view.MyNetworkImageView
android:id="@+id/ivMedia4"
@ -162,8 +144,7 @@
android:layout_height="match_parent"
android:layout_marginStart="4dp"
android:background="@drawable/btn_bg_transparent"
android:scaleType="fitCenter"
/>
android:scaleType="fitCenter" />
<CheckBox
android:id="@+id/cbNSFW"
@ -171,8 +152,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:layout_marginStart="4dp"
android:text="@string/nsfw"
/>
android:text="@string/nsfw" />
</LinearLayout>
<CheckBox
@ -180,30 +160,26 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/content_warning"
/>
android:text="@string/content_warning" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPostFormBackground"
>
android:background="?attr/colorPostFormBackground">
<jp.juggler.subwaytooter.view.MyEditText
android:id="@+id/etContentWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/content_warning_hint"
android:inputType="text"
/>
android:inputType="text" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:orientation="horizontal"
>
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
@ -211,8 +187,7 @@
android:layout_gravity="bottom"
android:layout_marginTop="8dp"
android:layout_weight="1"
android:text="@string/status"
/>
android:text="@string/status" />
<ImageButton
android:id="@+id/btnFeaturedTag"
@ -253,8 +228,7 @@
android:gravity="start|top"
android:hint="@string/content_hint"
android:inputType="textMultiLine"
android:minLines="5"
/>
android:minLines="5" />
</FrameLayout>
@ -262,14 +236,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/scheduled_status"
/>
android:text="@string/scheduled_status" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
android:orientation="horizontal">
<TextView
android:id="@+id/tvSchedule"
@ -277,8 +249,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:gravity="center"
/>
android:gravity="center" />
<ImageButton
android:id="@+id/ibSchedule"
@ -309,36 +280,31 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/make_enquete"
/>
android:text="@string/make_enquete" />
<LinearLayout
android:id="@+id/llEnquete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/choice1"
/>
android:text="@string/choice1" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPostFormBackground"
>
android:background="?attr/colorPostFormBackground">
<jp.juggler.subwaytooter.view.MyEditText
android:id="@+id/etChoice1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start|top"
android:inputType="text"
/>
android:inputType="text" />
</FrameLayout>
@ -346,22 +312,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/choice2"
/>
android:text="@string/choice2" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPostFormBackground"
>
android:background="?attr/colorPostFormBackground">
<jp.juggler.subwaytooter.view.MyEditText
android:id="@+id/etChoice2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start|top"
android:inputType="text"
/>
android:inputType="text" />
</FrameLayout>
@ -369,22 +332,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/choice3"
/>
android:text="@string/choice3" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPostFormBackground"
>
android:background="?attr/colorPostFormBackground">
<jp.juggler.subwaytooter.view.MyEditText
android:id="@+id/etChoice3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start|top"
android:inputType="text"
/>
android:inputType="text" />
</FrameLayout>
@ -392,22 +352,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/choice4"
/>
android:text="@string/choice4" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPostFormBackground"
>
android:background="?attr/colorPostFormBackground">
<jp.juggler.subwaytooter.view.MyEditText
android:id="@+id/etChoice4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start|top"
android:inputType="text"
/>
android:inputType="text" />
</FrameLayout>
@ -416,31 +373,27 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/allow_multiple_choice"
/>
android:text="@string/allow_multiple_choice" />
<CheckBox
android:id="@+id/cbHideTotals"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/hide_totals"
/>
android:text="@string/hide_totals" />
<LinearLayout
android:id="@+id/llExpire"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:orientation="horizontal"
>
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@string/expiration"
/>
android:text="@string/expiration" />
<EditText
android:id="@+id/etExpireDays"
@ -450,23 +403,21 @@
android:inputType="numberDecimal"
android:minWidth="48dp"
android:text="1"
tools:ignore="Autofill,HardcodedText"
tools:ignore="Autofill,HardcodedText,LabelFor"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/poll_expire_days"
/>
android:text="@string/poll_expire_days" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:text="@string/plus"
/>
android:text="@string/plus" />
<EditText
android:id="@+id/etExpireHours"
@ -475,22 +426,19 @@
android:gravity="center"
android:inputType="numberDecimal"
android:minWidth="48dp"
tools:ignore="Autofill,HardcodedText"
/>
tools:ignore="Autofill,HardcodedText,LabelFor" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/poll_expire_hours"
/>
android:text="@string/poll_expire_hours" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:text="@string/plus"
/>
android:text="@string/plus" />
<EditText
android:id="@+id/etExpireMinutes"
@ -499,14 +447,12 @@
android:gravity="center"
android:inputType="numberDecimal"
android:minWidth="48dp"
tools:ignore="Autofill,HardcodedText"
/>
tools:ignore="Autofill,HardcodedText,LabelFor" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/poll_expire_minutes"
/>
android:text="@string/poll_expire_minutes" />
</LinearLayout>
</LinearLayout>
@ -521,8 +467,7 @@
android:layout_height="48dp"
android:background="?attr/colorStatusButtonsPopupBg"
android:baselineAligned="false"
android:orientation="horizontal"
>
android:orientation="horizontal">
<ImageButton
android:id="@+id/btnAttachment"
@ -531,8 +476,7 @@
android:background="@drawable/btn_bg_transparent"
android:contentDescription="@string/media_attachment"
android:src="@drawable/ic_clip"
android:tint="?attr/colorVectorDrawable"
/>
android:tint="?attr/colorVectorDrawable" />
<ImageButton
android:id="@+id/btnVisibility"
@ -544,8 +488,7 @@
android:minWidth="48dp"
android:minHeight="48dp"
android:tint="?attr/colorVectorDrawable"
tools:src="@drawable/ic_public"
/>
tools:src="@drawable/ic_public" />
<ImageButton
android:id="@+id/btnPlugin"
@ -555,8 +498,7 @@
android:background="@drawable/btn_bg_transparent"
android:contentDescription="@string/plugin"
android:src="@drawable/ic_extension"
android:tint="?attr/colorVectorDrawable"
/>
android:tint="?attr/colorVectorDrawable" />
<ImageButton
android:id="@+id/btnMore"
@ -573,8 +515,7 @@
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
/>
android:layout_weight="1" />
<TextView
android:id="@+id/tvCharCount"
@ -585,8 +526,7 @@
android:gravity="end|center_vertical"
android:minWidth="32dp"
tools:text="-500"
tools:textColor="#f00"
/>
tools:textColor="#f00" />
<ImageButton
android:id="@+id/btnPost"

View File

@ -50,7 +50,7 @@
android:id="@+id/btnCopy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/copy"
android:text="@string/copy_st"
android:textAllCaps="false"
/>

View File

@ -110,6 +110,7 @@
android:layout_marginEnd="12dp"
android:text="@string/reason_create_account"
android:id="@+id/tvReasonCaption"
android:labelFor="@+id/etReason"
/>
<EditText

View File

@ -1,190 +1,177 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
android:orientation="vertical">
<jp.juggler.subwaytooter.view.MaxHeightScrollView
android:id="@+id/llColumnSetting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fadeScrollbars="false"
app:maxHeight="240dp"
>
app:maxHeight="240dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="3dp"
android:paddingBottom="3dp"
>
android:paddingEnd="12dp"
android:paddingBottom="3dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/visibility"
/>
android:text="@string/visibility" />
<Button
android:id="@+id/btnVisibility"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/btnVisibility"
android:gravity="start|center_vertical"
android:textAllCaps="false"
/>
android:textAllCaps="false" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fixed_phrase"
android:layout_marginTop="6dp"
/>
android:text="@string/fixed_phrase" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
android:layout_height="wrap_content">
<EditText
android:id="@+id/etText0"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="6dp"
android:layout_weight="1"
android:importantForAutofill="no"
android:inputType="text"
/>
tools:ignore="LabelFor" />
<Button
android:id="@+id/btnText0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:text="@string/input"
/>
android:text="@string/input" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
android:layout_height="wrap_content">
<EditText
android:id="@+id/etText1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="6dp"
android:layout_weight="1"
android:importantForAutofill="no"
android:inputType="text"
/>
tools:ignore="LabelFor" />
<Button
android:id="@+id/btnText1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:text="@string/input"
/>
android:text="@string/input" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
android:layout_height="wrap_content">
<EditText
android:id="@+id/etText2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="6dp"
android:layout_weight="1"
android:importantForAutofill="no"
android:inputType="text"
/>
tools:ignore="LabelFor" />
<Button
android:id="@+id/btnText2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:text="@string/input"
/>
android:text="@string/input" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
android:layout_height="wrap_content">
<EditText
android:id="@+id/etText3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="6dp"
android:layout_weight="1"
android:importantForAutofill="no"
android:inputType="text"
/>
tools:ignore="LabelFor" />
<Button
android:id="@+id/btnText3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:text="@string/input"
/>
android:text="@string/input" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
android:layout_height="wrap_content">
<EditText
android:id="@+id/etText4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="text"
android:layout_marginEnd="6dp"
/>
android:layout_weight="1"
android:importantForAutofill="no"
android:inputType="text"
tools:ignore="LabelFor" />
<Button
android:id="@+id/btnText4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:text="@string/input"
/>
android:text="@string/input" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
android:layout_height="wrap_content">
<EditText
android:id="@+id/etText5"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="text"
android:layout_marginEnd="6dp"
/>
android:layout_weight="1"
android:importantForAutofill="no"
android:inputType="text"
tools:ignore="LabelFor" />
<Button
android:id="@+id/btnText5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:text="@string/input"
/>
android:text="@string/input" />
</LinearLayout>
</LinearLayout>
</jp.juggler.subwaytooter.view.MaxHeightScrollView>
@ -193,8 +180,7 @@
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
android:orientation="horizontal">
<Button
android:id="@+id/btnCancel"
@ -202,7 +188,6 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/close"
/>
android:text="@string/close" />
</LinearLayout>
</LinearLayout>

View File

@ -1,37 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
android:orientation="vertical">
<TextView
android:id="@+id/tvCaption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:id="@+id/tvCaption"
android:layout_marginEnd="12dp"
android:labelFor="@+id/etInput"
/>
tools:ignore="LabelFor" />
<EditText
android:id="@+id/etInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:imeOptions="actionDone"
android:inputType="text"
/>
android:importantForAutofill="no"
android:inputType="text" />
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
android:orientation="horizontal">
<Button
android:id="@+id/btnCancel"
@ -39,8 +37,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/cancel"
/>
android:text="@string/cancel" />
<Button
android:id="@+id/btnOk"
@ -48,7 +45,6 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/ok"
/>
android:text="@string/ok" />
</LinearLayout>
</LinearLayout>

View File

@ -97,7 +97,7 @@
<string name="color_and_background">اللون والخلفية…</string>
<string name="column_background">خلفية العمود</string>
<string name="foreground_color">اللون الأمامي</string>
<string name="copy">نسخ</string>
<string name="copy_st">نسخ</string>
<string name="search_web">البحث على الويب</string>
<string name="muted_word">الكلمات المحظورة</string>
<string name="word_was_muted">تم حظر الكلمة.</string>

View File

@ -242,7 +242,7 @@
<string name="image">Delwedd</string>
<string name="column_header">Pennawd y golofn</string>
<string name="foreground_color">Lliw y blaendir</string>
<string name="copy">Copïo</string>
<string name="copy_st">Copïo</string>
<string name="send">Anfon</string>
<string name="mute_word">Gwahardd gair</string>
<string name="select_and_copy">Dewis a chopïo</string>

View File

@ -100,7 +100,7 @@
<string name="conversation">conversation</string>
<string name="conversation_around">Conversation du statut: %1$s</string>
<string name="conversation_view">Visualiser la conversation</string>
<string name="copy">Copier</string>
<string name="copy_st">Copier</string>
<string name="copy_complete">Presse-papiers mis à jour.</string>
<string name="default_status_visibility">Visibilité par défaut du pouet</string>
<string name="delete">Supprimer</string>

View File

@ -364,7 +364,7 @@
<string name="column_header">כותר העמודה</string>
<string name="foreground_color">צבע החזית</string>
<string name="hashtag_and_visibility_not_match">הודעה זו מכילה תגיות , אך נראות ההודעה אינה ציבורית. בדרך כלל ניתן לחפש תגיות רק בהודעות ציבוריות. האם אתה בטוח\?</string>
<string name="copy">העתק</string>
<string name="copy_st">העתק</string>
<string name="send">שלח</string>
<string name="mute_word">אסור מילה</string>
<string name="select_and_copy">בחר והעתק</string>

View File

@ -177,7 +177,7 @@
<string name="conversation_around">会話の流れ(id:%1$s)</string>
<string name="conversation_from_another_account">別アカウントで会話の流れ</string>
<string name="conversation_view">会話ビュー</string>
<string name="copy">コピー</string>
<string name="copy_st">コピー</string>
<string name="copy_complete">クリップボードにコピーしました</string>
<string name="copy_url">URLをクリップボードにコピー</string>
<string name="default_status_visibility">投稿の公開範囲の既定値</string>

View File

@ -305,7 +305,7 @@
<string name="column_header">칼럼 헤더</string>
<string name="foreground_color">전면색</string>
<string name="hashtag_and_visibility_not_match">이 메시지는 해시태그를 포함하고 있지만 공개 범위가 전체 공개가 아닙니다. 보통 해시태그는 공개 메시지에서만 검색 가능합니다. 계속하시겠습니까\?</string>
<string name="copy">복사</string>
<string name="copy_st">복사</string>
<string name="send">전송</string>
<string name="mute_word">금지 단어</string>
<string name="select_and_copy">선택과 복사</string>

View File

@ -255,7 +255,7 @@
<string name="pick_images">Velg bilde(r)…</string>
<string name="image">Bilde</string>
<string name="foreground_color">Forgrunnsfarge</string>
<string name="copy">Kopier</string>
<string name="copy_st">Kopier</string>
<string name="send">Send</string>
<string name="mute_word">Bannlys ord</string>
<string name="select_and_copy">Velg og kopier</string>

View File

@ -311,7 +311,7 @@
<string name="column_header">Column header</string>
<string name="foreground_color">Foreground color</string>
<string name="hashtag_and_visibility_not_match">this message contains hashtags, but message visibility is not public. normally hashtags are searchable only in public messages. Are you sure?</string>
<string name="copy">Copy</string>
<string name="copy_st">Copy</string>
<string name="send">Send</string>
<string name="mute_word">Ban word</string>
<string name="select_and_copy">Select and copy</string>

View File

@ -21,90 +21,93 @@ import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
/**
* This drawable will draw a simple white and gray chessboard pattern.
* It's the pattern you will often see as a background behind a partly transparent image in many applications.
*/
class AlphaPatternDrawable extends Drawable {
private int rectangleSize = 10;
private Paint paint = new Paint();
private Paint paintWhite = new Paint();
private Paint paintGray = new Paint();
private int numRectanglesHorizontal;
private int numRectanglesVertical;
/**
* Bitmap in which the pattern will be cached.
* This is so the pattern will not have to be recreated each time draw() gets called.
* Because recreating the pattern i rather expensive. I will only be recreated if the size changes.
*/
private Bitmap bitmap;
AlphaPatternDrawable(int rectangleSize) {
this.rectangleSize = rectangleSize;
paintWhite.setColor(0xFFFFFFFF);
paintGray.setColor(0xFFCBCBCB);
}
@Override public void draw(Canvas canvas) {
if (bitmap != null && !bitmap.isRecycled()) {
canvas.drawBitmap(bitmap, null, getBounds(), paint);
}
}
@Override public int getOpacity() {
return 0;
}
@Override public void setAlpha(int alpha) {
throw new UnsupportedOperationException("Alpha is not supported by this drawable.");
}
@Override public void setColorFilter(ColorFilter cf) {
throw new UnsupportedOperationException("ColorFilter is not supported by this drawable.");
}
@Override protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
int height = bounds.height();
int width = bounds.width();
numRectanglesHorizontal = (int) Math.ceil((width / rectangleSize));
numRectanglesVertical = (int) Math.ceil(height / rectangleSize);
generatePatternBitmap();
}
/**
* 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
*/
private void generatePatternBitmap() {
if (getBounds().width() <= 0 || getBounds().height() <= 0) {
return;
}
bitmap = Bitmap.createBitmap(getBounds().width(), getBounds().height(), Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Rect r = new Rect();
boolean verticalStartWhite = true;
for (int i = 0; i <= numRectanglesVertical; i++) {
boolean isWhite = verticalStartWhite;
for (int j = 0; j <= numRectanglesHorizontal; j++) {
r.top = i * rectangleSize;
r.left = j * rectangleSize;
r.bottom = r.top + rectangleSize;
r.right = r.left + rectangleSize;
canvas.drawRect(r, isWhite ? paintWhite : paintGray);
isWhite = !isWhite;
}
verticalStartWhite = !verticalStartWhite;
}
}
private int rectangleSize;
private Paint paint = new Paint();
private Paint paintWhite = new Paint();
private Paint paintGray = new Paint();
private int numRectanglesHorizontal;
private int numRectanglesVertical;
/**
* Bitmap in which the pattern will be cached.
* This is so the pattern will not have to be recreated each time draw() gets called.
* Because recreating the pattern i rather expensive. I will only be recreated if the size changes.
*/
private Bitmap bitmap;
AlphaPatternDrawable( int rectangleSize ){
this.rectangleSize = rectangleSize;
paintWhite.setColor( 0xFFFFFFFF );
paintGray.setColor( 0xFFCBCBCB );
}
@Override public void draw( @NonNull Canvas canvas ){
if( bitmap != null && ! bitmap.isRecycled() ){
canvas.drawBitmap( bitmap, null, getBounds(), paint );
}
}
@Override public int getOpacity(){
return PixelFormat.UNKNOWN;
}
@Override public void setAlpha( int alpha ){
throw new UnsupportedOperationException( "Alpha is not supported by this drawable." );
}
@Override public void setColorFilter( ColorFilter cf ){
throw new UnsupportedOperationException( "ColorFilter is not supported by this drawable." );
}
@Override protected void onBoundsChange( Rect bounds ){
super.onBoundsChange( bounds );
int height = bounds.height();
int width = bounds.width();
numRectanglesHorizontal = (int) Math.ceil( width / (float) rectangleSize );
numRectanglesVertical = (int) Math.ceil( height / (float) rectangleSize );
generatePatternBitmap();
}
/**
* 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
*/
private void generatePatternBitmap(){
if( getBounds().width() <= 0 || getBounds().height() <= 0 ){
return;
}
bitmap = Bitmap.createBitmap( getBounds().width(), getBounds().height(), Config.ARGB_8888 );
Canvas canvas = new Canvas( bitmap );
Rect r = new Rect();
boolean verticalStartWhite = true;
for( int i = 0 ; i <= numRectanglesVertical ; i++ ){
boolean isWhite = verticalStartWhite;
for( int j = 0 ; j <= numRectanglesHorizontal ; j++ ){
r.top = i * rectangleSize;
r.left = j * rectangleSize;
r.bottom = r.top + rectangleSize;
r.right = r.left + rectangleSize;
canvas.drawRect( r, isWhite ? paintWhite : paintGray );
isWhite = ! isWhite;
}
verticalStartWhite = ! verticalStartWhite;
}
}
}

View File

@ -1,97 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="RtlHardcoded"
>
tools:ignore="RtlHardcoded">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.jrummyapps.android.colorpicker.ColorPickerView
android:id="@id/cpv_color_picker_view"
style="@style/cpv_ColorPickerViewStyle"
android:padding="16dp"/>
<com.google.android.flexbox.FlexboxLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
app:flexWrap="wrap"
app:justifyContent="flex_start"
>
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@id/cpv_color_panel_old"
android:layout_width="@dimen/cpv_dialog_preview_width"
android:layout_height="@dimen/cpv_dialog_preview_height"
app:cpv_colorShape="square"/>
<com.jrummyapps.android.colorpicker.ColorPickerView
android:id="@id/cpv_color_picker_view"
style="@style/cpv_ColorPickerViewStyle"
android:padding="16dp" />
<ImageView
android:id="@+id/cpv_arrow_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:src="@drawable/cpv_ic_arrow_right_black_24dp"
tools:ignore="ContentDescription"/>
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@id/cpv_color_panel_new"
android:layout_width="@dimen/cpv_dialog_preview_width"
android:layout_height="@dimen/cpv_dialog_preview_height"
app:cpv_colorShape="square"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
android:gravity="right"
android:orientation="horizontal"
>
<TextView
android:layout_width="wrap_content"
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#"
android:typeface="monospace"
tools:ignore="HardcodedText"
android:labelFor="@+id/cpv_hex"
/>
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="16dp"
app:flexWrap="wrap"
app:justifyContent="flex_start">
<EditText
android:id="@+id/cpv_hex"
android:layout_width="110sp"
android:layout_height="wrap_content"
android:digits="0123456789ABCDEFabcdef"
android:focusable="true"
android:imeOptions="actionGo"
android:inputType="textNoSuggestions"
android:maxLength="8"
android:maxLines="1"
android:typeface="monospace"
tools:text="88888888"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</LinearLayout>
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@id/cpv_color_panel_old"
android:layout_width="@dimen/cpv_dialog_preview_width"
android:layout_height="@dimen/cpv_dialog_preview_height"
app:cpv_colorShape="square" />
</com.google.android.flexbox.FlexboxLayout>
<ImageView
android:id="@+id/cpv_arrow_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:src="@drawable/cpv_ic_arrow_right_black_24dp"
tools:ignore="ContentDescription" />
</LinearLayout>
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@id/cpv_color_panel_new"
android:layout_width="@dimen/cpv_dialog_preview_width"
android:layout_height="@dimen/cpv_dialog_preview_height"
app:cpv_colorShape="square" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
android:gravity="right"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:labelFor="@+id/cpv_hex"
android:text="#"
android:typeface="monospace"
tools:ignore="HardcodedText" />
<EditText
android:id="@+id/cpv_hex"
android:layout_width="110sp"
android:layout_height="wrap_content"
android:digits="0123456789ABCDEFabcdef"
android:focusable="true"
android:imeOptions="actionGo"
android:importantForAutofill="no"
android:inputType="textNoSuggestions"
android:maxLength="8"
android:maxLines="1"
android:typeface="monospace"
tools:text="88888888" />
</LinearLayout>
</com.google.android.flexbox.FlexboxLayout>
</LinearLayout>
</ScrollView>

View File

@ -1,24 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false">
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@+id/cpv_color_panel_view"
android:layout_width="@dimen/cpv_item_size"
android:layout_height="@dimen/cpv_item_size"
android:layout_gravity="center"
android:clickable="true"
app:cpv_colorShape="circle"/>
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@+id/cpv_color_panel_view"
android:layout_width="@dimen/cpv_item_size"
android:layout_height="@dimen/cpv_item_size"
android:layout_gravity="center"
android:clickable="true"
app:cpv_colorShape="circle"
tools:ignore="KeyboardInaccessibleWidget" />
<ImageView
android:id="@+id/cpv_color_image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:clickable="false"/>
<ImageView
android:id="@+id/cpv_color_image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:clickable="false"
android:importantForAccessibility="no" />
</FrameLayout>

View File

@ -1,24 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false">
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@+id/cpv_color_panel_view"
android:layout_width="@dimen/cpv_item_size"
android:layout_height="@dimen/cpv_item_size"
android:layout_gravity="center"
android:clickable="true"
app:cpv_colorShape="square"/>
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@+id/cpv_color_panel_view"
android:layout_width="@dimen/cpv_item_size"
android:layout_height="@dimen/cpv_item_size"
android:layout_gravity="center"
android:clickable="true"
app:cpv_colorShape="square"
tools:ignore="KeyboardInaccessibleWidget" />
<ImageView
android:id="@+id/cpv_color_image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:clickable="false"/>
<ImageView
android:id="@+id/cpv_color_image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:clickable="false"
android:importantForAccessibility="no" />
</FrameLayout>

View File

@ -1,49 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
android:layout_height="wrap_content">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
android:orientation="vertical">
<com.jrummyapps.android.colorpicker.ColorPickerView
android:id="@id/cpv_color_picker_view"
style="@style/cpv_ColorPickerViewStyle"
android:padding="16dp"
/>
android:padding="16dp" />
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="16dp"
app:flexWrap="wrap"
app:justifyContent="flex_start"
tools:ignore="RtlHardcoded"
>
tools:ignore="RtlHardcoded">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
android:layout_height="wrap_content">
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@id/cpv_color_panel_old"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:cpv_colorShape="square"
/>
app:cpv_colorShape="square" />
<ImageView
android:id="@+id/cpv_arrow_right"
@ -53,16 +44,14 @@
android:paddingLeft="3dp"
android:paddingRight="3dp"
android:src="@drawable/cpv_ic_arrow_right_black_24dp"
tools:ignore="ContentDescription"
/>
tools:ignore="ContentDescription" />
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@id/cpv_color_panel_new"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:cpv_colorShape="square"
/>
app:cpv_colorShape="square" />
</LinearLayout>
<LinearLayout
@ -73,16 +62,14 @@
android:focusableInTouchMode="true"
android:gravity="right"
android:orientation="horizontal"
tools:ignore="RtlHardcoded"
>
tools:ignore="RtlHardcoded">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#"
android:typeface="monospace"
tools:ignore="HardcodedText"
/>
tools:ignore="HardcodedText" />
<EditText
android:id="@+id/cpv_hex"
@ -91,13 +78,14 @@
android:digits="0123456789ABCDEFabcdef"
android:focusable="true"
android:imeOptions="actionGo"
android:importantForAutofill="no"
android:inputType="textNoSuggestions"
android:maxLength="8"
android:maxLines="1"
android:minWidth="110dp"
android:typeface="monospace"
tools:text="88888888"
/>
tools:ignore="LabelFor"
tools:text="88888888" />
</LinearLayout>
</com.google.android.flexbox.FlexboxLayout>

View File

@ -1,4 +1,6 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'
android {
compileSdkVersion target_sdk_version
@ -23,4 +25,10 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'commons-io:commons-io:2.6'
implementation 'androidx.annotation:annotation:1.1.0'
implementation "androidx.core:core-ktx:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
repositories {
mavenCentral()
}

View File

@ -1,48 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2;
import java.io.InputStream;
import java.nio.ByteBuffer;
class ByteBufferInputStream extends InputStream {
private ByteBuffer mBuf;
public ByteBufferInputStream( ByteBuffer buf ) {
mBuf = buf;
}
@Override
public int read() {
if( ! mBuf.hasRemaining() ) {
return - 1;
}
return mBuf.get() & 0xFF;
}
@Override
public int read( byte[] bytes, int off, int len ) {
if( ! mBuf.hasRemaining() ) {
return - 1;
}
len = Math.min( len, mBuf.remaining() );
mBuf.get( bytes, off, len );
return len;
}
}

View File

@ -14,18 +14,23 @@
* limitations under the License.
*/
package it.sephiroth.android.library.exif2;
package it.sephiroth.android.library.exif2
/**
* The constants of the IFD ID defined in EXIF spec.
*/
public interface IfdId {
public static final int TYPE_IFD_0 = 0;
public static final int TYPE_IFD_1 = 1;
public static final int TYPE_IFD_EXIF = 2;
public static final int TYPE_IFD_INTEROPERABILITY = 3;
public static final int TYPE_IFD_GPS = 4;
/* This is used in ExifData to allocate enough IfdData */
static final int TYPE_IFD_COUNT = 5;
import java.io.InputStream
import java.nio.ByteBuffer
import kotlin.math.min
internal class ByteBufferInputStream(private val mBuf : ByteBuffer) : InputStream() {
override fun read() : Int = when {
! mBuf.hasRemaining() -> - 1
else -> mBuf.get().toInt() and 0xFF
}
override fun read(bytes : ByteArray, off : Int, len : Int) : Int {
if(! mBuf.hasRemaining()) return - 1
val willRead = min(len, mBuf.remaining())
mBuf.get(bytes, off, willRead)
return willRead
}
}

View File

@ -1,156 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
class CountedDataInputStream extends FilterInputStream {
// allocate a byte buffer for a long value;
private final byte mByteArray[] = new byte[8];
private final ByteBuffer mByteBuffer = ByteBuffer.wrap( mByteArray );
private int mCount = 0;
private int mEnd = 0;
protected CountedDataInputStream( InputStream in ) {
super( in );
}
public void setEnd( int end ) {
mEnd = end;
}
public int getEnd() {
return mEnd;
}
public int getReadByteCount() {
return mCount;
}
@Override
public int read( byte[] b ) throws IOException {
int r = in.read( b );
mCount += ( r >= 0 ) ? r : 0;
return r;
}
@Override
public int read() throws IOException {
int r = in.read();
mCount += ( r >= 0 ) ? 1 : 0;
return r;
}
@Override
public int read( byte[] b, int off, int len ) throws IOException {
int r = in.read( b, off, len );
mCount += ( r >= 0 ) ? r : 0;
return r;
}
@Override
public long skip( long length ) throws IOException {
long skip = in.skip( length );
mCount += skip;
return skip;
}
public void skipTo( long target ) throws IOException {
long cur = mCount;
long diff = target - cur;
assert ( diff >= 0 );
skipOrThrow( diff );
}
public void skipOrThrow( long length ) throws IOException {
if( skip( length ) != length ) throw new EOFException();
}
public ByteOrder getByteOrder() {
return mByteBuffer.order();
}
public void setByteOrder( ByteOrder order ) {
mByteBuffer.order( order );
}
public int readUnsignedShort() throws IOException {
return readShort() & 0xffff;
}
public short readShort() throws IOException {
readOrThrow( mByteArray, 0, 2 );
mByteBuffer.rewind();
return mByteBuffer.getShort();
}
public byte readByte() throws IOException {
readOrThrow( mByteArray, 0, 1 );
mByteBuffer.rewind();
return mByteBuffer.get();
}
public int readUnsignedByte() throws IOException {
readOrThrow( mByteArray, 0, 1 );
mByteBuffer.rewind();
return (mByteBuffer.get() & 0xff);
}
public void readOrThrow( byte[] b, int off, int len ) throws IOException {
int r = read( b, off, len );
if( r != len ) throw new EOFException();
}
public long readUnsignedInt() throws IOException {
return readInt() & 0xffffffffL;
}
public int readInt() throws IOException {
readOrThrow( mByteArray, 0, 4 );
mByteBuffer.rewind();
return mByteBuffer.getInt();
}
public long readLong() throws IOException {
readOrThrow( mByteArray, 0, 8 );
mByteBuffer.rewind();
return mByteBuffer.getLong();
}
public String readString( int n ) throws IOException {
byte buf[] = new byte[n];
readOrThrow( buf );
return new String( buf, "UTF8" );
}
public void readOrThrow( byte[] b ) throws IOException {
readOrThrow( b, 0, b.length );
}
public String readString( int n, Charset charset ) throws IOException {
byte buf[] = new byte[n];
readOrThrow( buf );
return new String( buf, charset );
}
}

View File

@ -0,0 +1,149 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2
import java.io.EOFException
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
@Suppress("unused")
internal class CountedDataInputStream constructor(`in` : InputStream) :
FilterInputStream(`in`) {
// allocate a byte buffer for a long value;
private val mByteArray = ByteArray(8)
private val mByteBuffer = ByteBuffer.wrap(mByteArray)
var readByteCount = 0
private set
var end = 0
var byteOrder : ByteOrder
get() = mByteBuffer.order()
set(order) {
mByteBuffer.order(order)
}
@Throws(IOException::class)
override fun read(b : ByteArray) : Int {
val r = `in`.read(b)
readByteCount += if(r >= 0) r else 0
return r
}
@Throws(IOException::class)
override fun read() : Int {
val r = `in`.read()
readByteCount += if(r >= 0) 1 else 0
return r
}
@Throws(IOException::class)
override fun read(b : ByteArray, off : Int, len : Int) : Int {
val r = `in`.read(b, off, len)
readByteCount += if(r >= 0) r else 0
return r
}
@Throws(IOException::class)
override fun skip(length : Long) : Long {
val skip = `in`.skip(length)
readByteCount += skip.toInt()
return skip
}
@Throws(IOException::class)
fun skipTo(target : Long) {
val cur = readByteCount.toLong()
val diff = target - cur
if(diff < 0) throw IndexOutOfBoundsException("skipTo: negative move")
skipOrThrow(diff)
}
@Throws(IOException::class)
fun skipOrThrow(length : Long) {
if(skip(length) != length) throw EOFException()
}
@Throws(IOException::class)
fun readUnsignedShort() : Int = readShort().toInt() and 0xffff
@Throws(IOException::class)
fun readShort() : Short {
readOrThrow(mByteArray, 0, 2)
mByteBuffer.rewind()
return mByteBuffer.short
}
@Throws(IOException::class)
fun readByte() : Byte {
readOrThrow(mByteArray, 0, 1)
mByteBuffer.rewind()
return mByteBuffer.get()
}
@Throws(IOException::class)
fun readUnsignedByte() : Int {
readOrThrow(mByteArray, 0, 1)
mByteBuffer.rewind()
return mByteBuffer.get().toInt() and 0xff
}
@Throws(IOException::class)
@JvmOverloads
fun readOrThrow(b : ByteArray, off : Int = 0, len : Int = b.size) {
val r = read(b, off, len)
if(r != len) throw EOFException()
}
@Throws(IOException::class)
fun readUnsignedInt() : Long {
return readInt().toLong() and 0xffffffffL
}
@Throws(IOException::class)
fun readInt() : Int {
readOrThrow(mByteArray, 0, 4)
mByteBuffer.rewind()
return mByteBuffer.int
}
@Throws(IOException::class)
fun readLong() : Long {
readOrThrow(mByteArray, 0, 8)
mByteBuffer.rewind()
return mByteBuffer.long
}
@Throws(IOException::class)
fun readString(n : Int) : String {
val buf = ByteArray(n)
readOrThrow(buf)
return String(buf, StandardCharsets.UTF_8)
}
@Throws(IOException::class)
fun readString(n : Int, charset : Charset) : String {
val buf = ByteArray(n)
readOrThrow(buf)
return String(buf, charset)
}
}

View File

@ -1,383 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2;
import android.util.Log;
import java.io.UnsupportedEncodingException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* This class stores the EXIF header in IFDs according to the JPEG
* specification. It is the result produced by {@link ExifReader}.
*
* @see ExifReader
* @see IfdData
*/
class ExifData {
private static final String TAG = "ExifData";
private static final byte[] USER_COMMENT_ASCII = { 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00 };
private static final byte[] USER_COMMENT_JIS = { 0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00 };
private static final byte[] USER_COMMENT_UNICODE = { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00 };
private List<ExifParser.Section> mSections;
private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT];
private final ByteOrder mByteOrder;
private byte[] mThumbnail;
private ArrayList<byte[]> mStripBytes = new ArrayList<byte[]>();
private int qualityGuess = 0;
private int imageLength = -1, imageWidth = -1;
private short jpegProcess = 0;
public int mUncompressedDataPosition = 0;
ExifData( ByteOrder order ) {
mByteOrder = order;
}
/**
* Gets the compressed thumbnail. Returns null if there is no compressed
* thumbnail.
*
* @see #hasCompressedThumbnail()
*/
protected byte[] getCompressedThumbnail() {
return mThumbnail;
}
/**
* Sets the compressed thumbnail.
*/
protected void setCompressedThumbnail( byte[] thumbnail ) {
mThumbnail = thumbnail;
}
/**
* Returns true it this header contains a compressed thumbnail.
*/
protected boolean hasCompressedThumbnail() {
return mThumbnail != null;
}
/**
* Adds an uncompressed strip.
*/
protected void setStripBytes( int index, byte[] strip ) {
if( index < mStripBytes.size() ) {
mStripBytes.set( index, strip );
}
else {
for( int i = mStripBytes.size(); i < index; i++ ) {
mStripBytes.add( null );
}
mStripBytes.add( strip );
}
}
/**
* Gets the strip count.
*/
protected int getStripCount() {
return mStripBytes.size();
}
/**
* Gets the strip at the specified index.
*
* @exceptions #IndexOutOfBoundException
*/
protected byte[] getStrip( int index ) {
return mStripBytes.get( index );
}
/**
* Returns true if this header contains uncompressed strip.
*/
protected boolean hasUncompressedStrip() {
return mStripBytes.size() != 0;
}
/**
* Gets the byte order.
*/
protected ByteOrder getByteOrder() {
return mByteOrder;
}
/**
* Adds IFD data. If IFD data of the same type already exists, it will be
* replaced by the new data.
*/
protected void addIfdData( IfdData data ) {
mIfdDatas[data.getId()] = data;
}
/**
* Returns the tag with a given TID in the given IFD if the tag exists.
* Otherwise returns null.
*/
protected ExifTag getTag( short tag, int ifd ) {
IfdData ifdData = mIfdDatas[ifd];
return ( ifdData == null ) ? null : ifdData.getTag( tag );
}
/**
* Adds the given ExifTag to its default IFD and returns an existing ExifTag
* with the same TID or null if none exist.
*/
protected ExifTag addTag( ExifTag tag ) {
if( tag != null ) {
int ifd = tag.getIfd();
return addTag( tag, ifd );
}
return null;
}
/**
* Adds the given ExifTag to the given IFD and returns an existing ExifTag
* with the same TID or null if none exist.
*/
protected ExifTag addTag( ExifTag tag, int ifdId ) {
if( tag != null && ExifTag.isValidIfd( ifdId ) ) {
IfdData ifdData = getOrCreateIfdData( ifdId );
return ifdData.setTag( tag );
}
return null;
}
/**
* Returns the {@link IfdData} object corresponding to a given IFD or
* generates one if none exist.
*/
protected IfdData getOrCreateIfdData( int ifdId ) {
IfdData ifdData = mIfdDatas[ifdId];
if( ifdData == null ) {
ifdData = new IfdData( ifdId );
mIfdDatas[ifdId] = ifdData;
}
return ifdData;
}
/**
* Removes the thumbnail and its related tags. IFD1 will be removed.
*/
protected void removeThumbnailData() {
clearThumbnailAndStrips();
mIfdDatas[IfdId.TYPE_IFD_1] = null;
}
protected void clearThumbnailAndStrips() {
mThumbnail = null;
mStripBytes.clear();
}
/**
* Removes the tag with a given TID and IFD.
*/
protected void removeTag( short tagId, int ifdId ) {
IfdData ifdData = mIfdDatas[ifdId];
if( ifdData == null ) {
return;
}
ifdData.removeTag( tagId );
}
/**
* Decodes the user comment tag into string as specified in the EXIF
* standard. Returns null if decoding failed.
*/
protected String getUserComment() {
IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_0];
if( ifdData == null ) {
return null;
}
ExifTag tag = ifdData.getTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_USER_COMMENT ) );
if( tag == null ) {
return null;
}
if( tag.getComponentCount() < 8 ) {
return null;
}
byte[] buf = new byte[tag.getComponentCount()];
tag.getBytes( buf );
byte[] code = new byte[8];
System.arraycopy( buf, 0, code, 0, 8 );
try {
if( Arrays.equals( code, USER_COMMENT_ASCII ) ) {
return new String( buf, 8, buf.length - 8, "US-ASCII" );
}
else if( Arrays.equals( code, USER_COMMENT_JIS ) ) {
return new String( buf, 8, buf.length - 8, "EUC-JP" );
}
else if( Arrays.equals( code, USER_COMMENT_UNICODE ) ) {
return new String( buf, 8, buf.length - 8, "UTF-16" );
}
else {
return null;
}
} catch( UnsupportedEncodingException e ) {
Log.w( TAG, "Failed to decode the user comment" );
return null;
}
}
/**
* Returns a list of all {@link ExifTag}s in the ExifData or null if there
* are none.
*/
protected List<ExifTag> getAllTags() {
ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
for( IfdData d : mIfdDatas ) {
if( d != null ) {
ExifTag[] tags = d.getAllTags();
if( tags != null ) {
for( ExifTag t : tags ) {
ret.add( t );
}
}
}
}
if( ret.size() == 0 ) {
return null;
}
return ret;
}
/**
* Returns a list of all {@link ExifTag}s in a given IFD or null if there
* are none.
*/
protected List<ExifTag> getAllTagsForIfd( int ifd ) {
IfdData d = mIfdDatas[ifd];
if( d == null ) {
return null;
}
ExifTag[] tags = d.getAllTags();
if( tags == null ) {
return null;
}
ArrayList<ExifTag> ret = new ArrayList<ExifTag>( tags.length );
for( ExifTag t : tags ) {
ret.add( t );
}
if( ret.size() == 0 ) {
return null;
}
return ret;
}
/**
* Returns a list of all {@link ExifTag}s with a given TID or null if there
* are none.
*/
protected List<ExifTag> getAllTagsForTagId( short tag ) {
ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
for( IfdData d : mIfdDatas ) {
if( d != null ) {
ExifTag t = d.getTag( tag );
if( t != null ) {
ret.add( t );
}
}
}
if( ret.size() == 0 ) {
return null;
}
return ret;
}
@Override
public boolean equals( Object obj ) {
if( this == obj ) {
return true;
}
if( obj == null ) {
return false;
}
if( obj instanceof ExifData ) {
ExifData data = (ExifData) obj;
if( data.mByteOrder != mByteOrder ||
data.mStripBytes.size() != mStripBytes.size() ||
! Arrays.equals( data.mThumbnail, mThumbnail ) ) {
return false;
}
for( int i = 0; i < mStripBytes.size(); i++ ) {
if( ! Arrays.equals( data.mStripBytes.get( i ), mStripBytes.get( i ) ) ) {
return false;
}
}
for( int i = 0; i < IfdId.TYPE_IFD_COUNT; i++ ) {
IfdData ifd1 = data.getIfdData( i );
IfdData ifd2 = getIfdData( i );
if( ifd1 != ifd2 && ifd1 != null && ! ifd1.equals( ifd2 ) ) {
return false;
}
}
return true;
}
return false;
}
/**
* Returns the {@link IfdData} object corresponding to a given IFD if it
* exists or null.
*/
protected IfdData getIfdData( int ifdId ) {
if( ExifTag.isValidIfd( ifdId ) ) {
return mIfdDatas[ifdId];
}
return null;
}
protected void setQualityGuess( final int qualityGuess ) {
this.qualityGuess = qualityGuess;
}
public int getQualityGuess() {
return qualityGuess;
}
protected void setImageSize( final int imageWidth, final int imageLength ) {
this.imageWidth = imageWidth;
this.imageLength = imageLength;
}
public int[] getImageSize() {
return new int[]{ imageWidth, imageLength };
}
public void setJpegProcess( final short jpegProcess ) {
this.jpegProcess = jpegProcess;
}
public short getJpegProcess() {
return this.jpegProcess;
}
public void setSections( final List<ExifParser.Section> sections ) {
mSections = sections;
}
public List<ExifParser.Section> getSections() {
return mSections;
}
}

View File

@ -0,0 +1,335 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2
import android.util.Log
import java.io.UnsupportedEncodingException
import java.nio.ByteOrder
import java.nio.charset.Charset
import java.util.ArrayList
import java.util.Arrays
/**
* This class stores the EXIF header in IFDs according to the JPEG
* specification. It is the result produced by [ExifReader].
*
* @see ExifReader
*
* @see IfdData
*/
@Suppress("unused")
internal open class ExifData(
/**
* Gets the byte order.
*/
val byteOrder : ByteOrder
) {
var sections : List<ExifParser.Section>? = null
private val mIfdDatas = arrayOfNulls<IfdData>(IfdId.TYPE_IFD_COUNT)
/**
* Gets the compressed thumbnail. Returns null if there is no compressed
* thumbnail.
*
* @see .hasCompressedThumbnail
*/
/**
* Sets the compressed thumbnail.
*/
var compressedThumbnail : ByteArray? = null
private val mStripBytes = ArrayList<ByteArray?>()
var qualityGuess = 0
private var imageLength = - 1
private var imageWidth = - 1
var jpegProcess : Short = 0
var mUncompressedDataPosition = 0
/**
* Gets the strip count.
*/
val stripCount : Int
get() = mStripBytes.size
/**
* Decodes the user comment tag into string as specified in the EXIF
* standard. Returns null if decoding failed.
*/
val userComment : String?
get() {
val ifdData = mIfdDatas[IfdId.TYPE_IFD_0] ?: return null
val tag = ifdData.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT))
?: return null
if(tag.componentCount < 8) {
return null
}
val buf = ByteArray(tag.componentCount)
tag.getBytes(buf)
val code = ByteArray(8)
System.arraycopy(buf, 0, code, 0, 8)
return try {
when {
code.contentEquals(USER_COMMENT_ASCII) -> String(
buf,
8,
buf.size - 8,
Charsets.US_ASCII
)
code.contentEquals(USER_COMMENT_JIS) -> String(
buf,
8,
buf.size - 8,
Charset.forName("EUC-JP")
)
code.contentEquals(USER_COMMENT_UNICODE) -> String(
buf,
8,
buf.size - 8,
Charsets.UTF_16
)
else -> null
}
} catch(e : UnsupportedEncodingException) {
Log.w(TAG, "Failed to decode the user comment")
null
}
}
/**
* Returns a list of all [ExifTag]s in the ExifData or null if there
* are none.
*/
val allTags : List<ExifTag>?
get() {
val ret = ArrayList<ExifTag>()
mIfdDatas.forEach { it?.allTags?.forEach { tag->ret.add(tag) } }
return if(ret.isEmpty()) null else ret
}
val imageSize : IntArray
get() = intArrayOf(imageWidth, imageLength)
/**
* Returns true it this header contains a compressed thumbnail.
*/
fun hasCompressedThumbnail() : Boolean {
return compressedThumbnail != null
}
/**
* Adds an uncompressed strip.
*/
fun setStripBytes(index : Int, strip : ByteArray) {
if(index < mStripBytes.size) {
mStripBytes[index] = strip
} else {
for(i in mStripBytes.size until index) {
mStripBytes.add(null)
}
mStripBytes.add(strip)
}
}
/**
* Gets the strip at the specified index.
*
* @exceptions #IndexOutOfBoundException
*/
fun getStrip(index : Int) : ByteArray? = mStripBytes[index]
/**
* Returns true if this header contains uncompressed strip.
*/
fun hasUncompressedStrip() : Boolean = mStripBytes.isNotEmpty()
/**
* Adds IFD data. If IFD data of the same type already exists, it will be
* replaced by the new data.
*/
fun addIfdData(data : IfdData) {
mIfdDatas[data.id] = data
}
/**
* Returns the tag with a given TID in the given IFD if the tag exists.
* Otherwise returns null.
*/
fun getTag(tag : Short, ifd : Int) : ExifTag? {
val ifdData = mIfdDatas[ifd]
return ifdData?.getTag(tag)
}
/**
* Adds the given ExifTag to its default IFD and returns an existing ExifTag
* with the same TID or null if none exist.
*/
fun addTag(tag : ExifTag?) : ExifTag? {
if(tag != null) {
val ifd = tag.ifd
return addTag(tag, ifd)
}
return null
}
/**
* Adds the given ExifTag to the given IFD and returns an existing ExifTag
* with the same TID or null if none exist.
*/
private fun addTag(tag : ExifTag?, ifdId : Int) : ExifTag? {
if(tag != null && ExifTag.isValidIfd(ifdId)) {
val ifdData = getOrCreateIfdData(ifdId)
return ifdData.setTag(tag)
}
return null
}
/**
* Returns the [IfdData] object corresponding to a given IFD or
* generates one if none exist.
*/
private fun getOrCreateIfdData(ifdId : Int) : IfdData {
var ifdData : IfdData? = mIfdDatas[ifdId]
if(ifdData == null) {
ifdData = IfdData(ifdId)
mIfdDatas[ifdId] = ifdData
}
return ifdData
}
/**
* Removes the thumbnail and its related tags. IFD1 will be removed.
*/
protected fun removeThumbnailData() {
clearThumbnailAndStrips()
mIfdDatas[IfdId.TYPE_IFD_1] = null
}
fun clearThumbnailAndStrips() {
compressedThumbnail = null
mStripBytes.clear()
}
/**
* Removes the tag with a given TID and IFD.
*/
fun removeTag(tagId : Short, ifdId : Int) {
val ifdData = mIfdDatas[ifdId] ?: return
ifdData.removeTag(tagId)
}
/**
* Returns a list of all [ExifTag]s in a given IFD or null if there
* are none.
*/
fun getAllTagsForIfd(ifd : Int) : List<ExifTag>? {
val d = mIfdDatas[ifd] ?: return null
val tags = d.allTags ?: return null
val ret = ArrayList<ExifTag>(tags.size)
for(t in tags) {
ret.add(t)
}
return if(ret.size == 0) {
null
} else ret
}
// Returns a list of all [ExifTag]s with a given TID
// or null if there are none.
fun getAllTagsForTagId(tag : Short) : List<ExifTag>? {
val ret = ArrayList<ExifTag>()
for(d in mIfdDatas) {
if(d != null) {
val t = d.getTag(tag)
if(t != null) {
ret.add(t)
}
}
}
return if(ret.isEmpty()) null else ret
}
override fun equals(other : Any?) : Boolean {
if(this === other) return true
if(other is ExifData) {
if(other.byteOrder != byteOrder
|| other.mStripBytes.size != mStripBytes.size
|| ! Arrays.equals(other.compressedThumbnail, compressedThumbnail)
) {
return false
}
for(i in mStripBytes.indices) {
val a = mStripBytes[i]
val b = other.mStripBytes[i]
if(a != null && b != null) {
if(! a.contentEquals(b)) return false // 内容が異なる
} else if((a == null) xor (b == null)) {
return false // 片方だけnull
}
}
for(i in 0 until IfdId.TYPE_IFD_COUNT) {
val ifd1 = other.getIfdData(i)
val ifd2 = getIfdData(i)
if(ifd1 != ifd2) return false
}
return true
}
return false
}
/**
* Returns the [IfdData] object corresponding to a given IFD if it
* exists or null.
*/
fun getIfdData(ifdId : Int) : IfdData? {
return if(ExifTag.isValidIfd(ifdId)) {
mIfdDatas[ifdId]
} else null
}
fun setImageSize(imageWidth : Int, imageLength : Int) {
this.imageWidth = imageWidth
this.imageLength = imageLength
}
override fun hashCode() : Int {
var result = byteOrder.hashCode()
result = 31 * result + (sections?.hashCode() ?: 0)
result = 31 * result + mIfdDatas.contentHashCode()
result = 31 * result + (compressedThumbnail?.contentHashCode() ?: 0)
result = 31 * result + mStripBytes.hashCode()
result = 31 * result + qualityGuess
result = 31 * result + imageLength
result = 31 * result + imageWidth
result = 31 * result + jpegProcess
result = 31 * result + mUncompressedDataPosition
return result
}
companion object {
private const val TAG = "ExifData"
private val USER_COMMENT_ASCII = byteArrayOf(0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00)
private val USER_COMMENT_JIS = byteArrayOf(0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00)
private val USER_COMMENT_UNICODE =
byteArrayOf(0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -14,10 +14,6 @@
* limitations under the License.
*/
package it.sephiroth.android.library.exif2;
package it.sephiroth.android.library.exif2
public class ExifInvalidFormatException extends Exception {
public ExifInvalidFormatException( String meg ) {
super( meg );
}
}
class ExifInvalidFormatException(meg : String) : Exception(meg)

View File

@ -1,379 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
class ExifOutputStream {
private static final String TAG = "ExifOutputStream";
private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb
private static final int STATE_SOI = 0;
private static final int EXIF_HEADER = 0x45786966;
private static final short TIFF_HEADER = 0x002A;
private static final short TIFF_BIG_ENDIAN = 0x4d4d;
private static final short TIFF_LITTLE_ENDIAN = 0x4949;
private static final short TAG_SIZE = 12;
private static final short TIFF_HEADER_SIZE = 8;
private static final int MAX_EXIF_SIZE = 65535;
private final ExifInterface mInterface;
private ExifData mExifData;
private ByteBuffer mBuffer = ByteBuffer.allocate( 4 );
protected ExifOutputStream( ExifInterface iRef ) {
mInterface = iRef;
}
/**
* Gets the Exif header to be written into the JPEF file.
*/
protected ExifData getExifData() {
return mExifData;
}
/**
* Sets the ExifData to be written into the JPEG file. Should be called
* before writing image data.
*/
protected void setExifData( ExifData exifData ) {
mExifData = exifData;
}
private int requestByteToBuffer(
int requestByteCount, byte[] buffer, int offset, int length ) {
int byteNeeded = requestByteCount - mBuffer.position();
int byteToRead = length > byteNeeded ? byteNeeded : length;
mBuffer.put( buffer, offset, byteToRead );
return byteToRead;
}
public void writeExifData( OutputStream out ) throws IOException {
if( mExifData == null ) {
return;
}
Log.v( TAG, "Writing exif data..." );
ArrayList<ExifTag> nullTags = stripNullValueTags( mExifData );
createRequiredIfdAndTag();
int exifSize = calculateAllOffset();
// Log.i(TAG, "exifSize: " + (exifSize + 8));
if( exifSize + 8 > MAX_EXIF_SIZE ) {
throw new IOException( "Exif header is too large (>64Kb)" );
}
BufferedOutputStream outputStream = new BufferedOutputStream( out, STREAMBUFFER_SIZE );
OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream( outputStream );
dataOutputStream.setByteOrder( ByteOrder.BIG_ENDIAN );
dataOutputStream.write( 0xFF );
dataOutputStream.write( JpegHeader.TAG_M_EXIF );
dataOutputStream.writeShort( (short) ( exifSize + 8 ) );
dataOutputStream.writeInt( EXIF_HEADER );
dataOutputStream.writeShort( (short) 0x0000 );
if( mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN ) {
dataOutputStream.writeShort( TIFF_BIG_ENDIAN );
}
else {
dataOutputStream.writeShort( TIFF_LITTLE_ENDIAN );
}
dataOutputStream.setByteOrder( mExifData.getByteOrder() );
dataOutputStream.writeShort( TIFF_HEADER );
dataOutputStream.writeInt( 8 );
writeAllTags( dataOutputStream );
writeThumbnail( dataOutputStream );
for( ExifTag t : nullTags ) {
mExifData.addTag( t );
}
dataOutputStream.flush();
}
private ArrayList<ExifTag> stripNullValueTags( ExifData data ) {
ArrayList<ExifTag> nullTags = new ArrayList<ExifTag>();
for( ExifTag t : data.getAllTags() ) {
if( t.getValue() == null && ! ExifInterface.isOffsetTag( t.getTagId() ) ) {
data.removeTag( t.getTagId(), t.getIfd() );
nullTags.add( t );
}
}
return nullTags;
}
private void writeThumbnail( OrderedDataOutputStream dataOutputStream ) throws IOException {
if( mExifData.hasCompressedThumbnail() ) {
Log.d( TAG, "writing thumbnail.." );
dataOutputStream.write( mExifData.getCompressedThumbnail() );
}
else if( mExifData.hasUncompressedStrip() ) {
Log.d( TAG, "writing uncompressed strip.." );
for( int i = 0; i < mExifData.getStripCount(); i++ ) {
dataOutputStream.write( mExifData.getStrip( i ) );
}
}
}
private void writeAllTags( OrderedDataOutputStream dataOutputStream ) throws IOException {
writeIfd( mExifData.getIfdData( IfdId.TYPE_IFD_0 ), dataOutputStream );
writeIfd( mExifData.getIfdData( IfdId.TYPE_IFD_EXIF ), dataOutputStream );
IfdData interoperabilityIfd = mExifData.getIfdData( IfdId.TYPE_IFD_INTEROPERABILITY );
if( interoperabilityIfd != null ) {
writeIfd( interoperabilityIfd, dataOutputStream );
}
IfdData gpsIfd = mExifData.getIfdData( IfdId.TYPE_IFD_GPS );
if( gpsIfd != null ) {
writeIfd( gpsIfd, dataOutputStream );
}
IfdData ifd1 = mExifData.getIfdData( IfdId.TYPE_IFD_1 );
if( ifd1 != null ) {
writeIfd( mExifData.getIfdData( IfdId.TYPE_IFD_1 ), dataOutputStream );
}
}
private void writeIfd( IfdData ifd, OrderedDataOutputStream dataOutputStream ) throws IOException {
ExifTag[] tags = ifd.getAllTags();
dataOutputStream.writeShort( (short) tags.length );
for( ExifTag tag : tags ) {
dataOutputStream.writeShort( tag.getTagId() );
dataOutputStream.writeShort( tag.getDataType() );
dataOutputStream.writeInt( tag.getComponentCount() );
// Log.v( TAG, "\n" + tag.toString() );
if( tag.getDataSize() > 4 ) {
dataOutputStream.writeInt( tag.getOffset() );
}
else {
ExifOutputStream.writeTagValue( tag, dataOutputStream );
for( int i = 0, n = 4 - tag.getDataSize(); i < n; i++ ) {
dataOutputStream.write( 0 );
}
}
}
dataOutputStream.writeInt( ifd.getOffsetToNextIfd() );
for( ExifTag tag : tags ) {
if( tag.getDataSize() > 4 ) {
ExifOutputStream.writeTagValue( tag, dataOutputStream );
}
}
}
private int calculateOffsetOfIfd( IfdData ifd, int offset ) {
offset += 2 + ifd.getTagCount() * TAG_SIZE + 4;
ExifTag[] tags = ifd.getAllTags();
for( ExifTag tag : tags ) {
if( tag.getDataSize() > 4 ) {
tag.setOffset( offset );
offset += tag.getDataSize();
}
}
return offset;
}
private void createRequiredIfdAndTag() throws IOException {
// IFD0 is required for all file
IfdData ifd0 = mExifData.getIfdData( IfdId.TYPE_IFD_0 );
if( ifd0 == null ) {
ifd0 = new IfdData( IfdId.TYPE_IFD_0 );
mExifData.addIfdData( ifd0 );
}
ExifTag exifOffsetTag = mInterface.buildUninitializedTag( ExifInterface.TAG_EXIF_IFD );
if( exifOffsetTag == null ) {
throw new IOException( "No definition for crucial exif tag: " + ExifInterface.TAG_EXIF_IFD );
}
ifd0.setTag( exifOffsetTag );
// Exif IFD is required for all files.
IfdData exifIfd = mExifData.getIfdData( IfdId.TYPE_IFD_EXIF );
if( exifIfd == null ) {
exifIfd = new IfdData( IfdId.TYPE_IFD_EXIF );
mExifData.addIfdData( exifIfd );
}
// GPS IFD
IfdData gpsIfd = mExifData.getIfdData( IfdId.TYPE_IFD_GPS );
if( gpsIfd != null ) {
ExifTag gpsOffsetTag = mInterface.buildUninitializedTag( ExifInterface.TAG_GPS_IFD );
if( gpsOffsetTag == null ) {
throw new IOException( "No definition for crucial exif tag: " + ExifInterface.TAG_GPS_IFD );
}
ifd0.setTag( gpsOffsetTag );
}
// Interoperability IFD
IfdData interIfd = mExifData.getIfdData( IfdId.TYPE_IFD_INTEROPERABILITY );
if( interIfd != null ) {
ExifTag interOffsetTag = mInterface.buildUninitializedTag( ExifInterface.TAG_INTEROPERABILITY_IFD );
if( interOffsetTag == null ) {
throw new IOException( "No definition for crucial exif tag: " + ExifInterface.TAG_INTEROPERABILITY_IFD );
}
exifIfd.setTag( interOffsetTag );
}
IfdData ifd1 = mExifData.getIfdData( IfdId.TYPE_IFD_1 );
// thumbnail
if( mExifData.hasCompressedThumbnail() ) {
if( ifd1 == null ) {
ifd1 = new IfdData( IfdId.TYPE_IFD_1 );
mExifData.addIfdData( ifd1 );
}
ExifTag offsetTag = mInterface.buildUninitializedTag( ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT );
if( offsetTag == null ) {
throw new IOException( "No definition for crucial exif tag: " + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT );
}
ifd1.setTag( offsetTag );
ExifTag lengthTag = mInterface.buildUninitializedTag( ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH );
if( lengthTag == null ) {
throw new IOException( "No definition for crucial exif tag: " + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH );
}
lengthTag.setValue( mExifData.getCompressedThumbnail().length );
ifd1.setTag( lengthTag );
// Get rid of tags for uncompressed if they exist.
ifd1.removeTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_STRIP_OFFSETS ) );
ifd1.removeTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_STRIP_BYTE_COUNTS ) );
}
else if( mExifData.hasUncompressedStrip() ) {
if( ifd1 == null ) {
ifd1 = new IfdData( IfdId.TYPE_IFD_1 );
mExifData.addIfdData( ifd1 );
}
int stripCount = mExifData.getStripCount();
ExifTag offsetTag = mInterface.buildUninitializedTag( ExifInterface.TAG_STRIP_OFFSETS );
if( offsetTag == null ) {
throw new IOException( "No definition for crucial exif tag: " + ExifInterface.TAG_STRIP_OFFSETS );
}
ExifTag lengthTag = mInterface.buildUninitializedTag( ExifInterface.TAG_STRIP_BYTE_COUNTS );
if( lengthTag == null ) {
throw new IOException( "No definition for crucial exif tag: " + ExifInterface.TAG_STRIP_BYTE_COUNTS );
}
long[] lengths = new long[stripCount];
for( int i = 0; i < mExifData.getStripCount(); i++ ) {
lengths[i] = mExifData.getStrip( i ).length;
}
lengthTag.setValue( lengths );
ifd1.setTag( offsetTag );
ifd1.setTag( lengthTag );
// Get rid of tags for compressed if they exist.
ifd1.removeTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT ) );
ifd1.removeTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH ) );
}
else if( ifd1 != null ) {
// Get rid of offset and length tags if there is no thumbnail.
ifd1.removeTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_STRIP_OFFSETS ) );
ifd1.removeTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_STRIP_BYTE_COUNTS ) );
ifd1.removeTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT ) );
ifd1.removeTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH ) );
}
}
private int calculateAllOffset() {
int offset = TIFF_HEADER_SIZE;
IfdData ifd0 = mExifData.getIfdData( IfdId.TYPE_IFD_0 );
offset = calculateOffsetOfIfd( ifd0, offset );
ifd0.getTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_EXIF_IFD ) ).setValue( offset );
IfdData exifIfd = mExifData.getIfdData( IfdId.TYPE_IFD_EXIF );
offset = calculateOffsetOfIfd( exifIfd, offset );
IfdData interIfd = mExifData.getIfdData( IfdId.TYPE_IFD_INTEROPERABILITY );
if( interIfd != null ) {
exifIfd.getTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_INTEROPERABILITY_IFD ) ).setValue( offset );
offset = calculateOffsetOfIfd( interIfd, offset );
}
IfdData gpsIfd = mExifData.getIfdData( IfdId.TYPE_IFD_GPS );
if( gpsIfd != null ) {
ifd0.getTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_GPS_IFD ) ).setValue( offset );
offset = calculateOffsetOfIfd( gpsIfd, offset );
}
IfdData ifd1 = mExifData.getIfdData( IfdId.TYPE_IFD_1 );
if( ifd1 != null ) {
ifd0.setOffsetToNextIfd( offset );
offset = calculateOffsetOfIfd( ifd1, offset );
}
// thumbnail
if( mExifData.hasCompressedThumbnail() ) {
ifd1.getTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT ) ).setValue( offset );
offset += mExifData.getCompressedThumbnail().length;
}
else if( mExifData.hasUncompressedStrip() ) {
int stripCount = mExifData.getStripCount();
long[] offsets = new long[stripCount];
for( int i = 0; i < mExifData.getStripCount(); i++ ) {
offsets[i] = offset;
offset += mExifData.getStrip( i ).length;
}
ifd1.getTag( ExifInterface.getTrueTagKey( ExifInterface.TAG_STRIP_OFFSETS ) ).setValue( offsets );
}
return offset;
}
static void writeTagValue( ExifTag tag, OrderedDataOutputStream dataOutputStream ) throws IOException {
switch( tag.getDataType() ) {
case ExifTag.TYPE_ASCII:
byte buf[] = tag.getStringByte();
if( buf.length == tag.getComponentCount() ) {
buf[buf.length - 1] = 0;
dataOutputStream.write( buf );
}
else {
dataOutputStream.write( buf );
dataOutputStream.write( 0 );
}
break;
case ExifTag.TYPE_LONG:
case ExifTag.TYPE_UNSIGNED_LONG:
for( int i = 0, n = tag.getComponentCount(); i < n; i++ ) {
dataOutputStream.writeInt( (int) tag.getValueAt( i ) );
}
break;
case ExifTag.TYPE_RATIONAL:
case ExifTag.TYPE_UNSIGNED_RATIONAL:
for( int i = 0, n = tag.getComponentCount(); i < n; i++ ) {
dataOutputStream.writeRational( tag.getRational( i ) );
}
break;
case ExifTag.TYPE_UNDEFINED:
case ExifTag.TYPE_UNSIGNED_BYTE:
buf = new byte[tag.getComponentCount()];
tag.getBytes( buf );
dataOutputStream.write( buf );
break;
case ExifTag.TYPE_UNSIGNED_SHORT:
for( int i = 0, n = tag.getComponentCount(); i < n; i++ ) {
dataOutputStream.writeShort( (short) tag.getValueAt( i ) );
}
break;
}
}
}

View File

@ -0,0 +1,375 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2
import android.util.Log
import java.io.BufferedOutputStream
import java.io.IOException
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.ArrayList
@Suppress("unused")
internal class ExifOutputStream(private val mInterface : ExifInterface) {
/**
* Gets the Exif header to be written into the JPEF file.
*/
/**
* Sets the ExifData to be written into the JPEG file. Should be called
* before writing image data.
*/
var exifData : ExifData? = null
private val mBuffer = ByteBuffer.allocate(4)
private fun requestByteToBuffer(
requestByteCount : Int, buffer : ByteArray, offset : Int, length : Int
) : Int {
val byteNeeded = requestByteCount - mBuffer.position()
val byteToRead = if(length > byteNeeded) byteNeeded else length
mBuffer.put(buffer, offset, byteToRead)
return byteToRead
}
@Throws(IOException::class)
fun writeExifData(out : OutputStream) {
if(exifData == null) {
return
}
Log.v(TAG, "Writing exif data...")
val nullTags = stripNullValueTags(exifData !!)
createRequiredIfdAndTag()
val exifSize = calculateAllOffset()
// Log.i(TAG, "exifSize: " + (exifSize + 8));
if(exifSize + 8 > MAX_EXIF_SIZE) {
throw IOException("Exif header is too large (>64Kb)")
}
val outputStream = BufferedOutputStream(out, STREAMBUFFER_SIZE)
val dataOutputStream = OrderedDataOutputStream(outputStream)
dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN)
dataOutputStream.write(0xFF)
dataOutputStream.write(JpegHeader.TAG_M_EXIF)
dataOutputStream.writeShort((exifSize + 8).toShort())
dataOutputStream.writeInt(EXIF_HEADER)
dataOutputStream.writeShort(0x0000.toShort())
if(exifData !!.byteOrder == ByteOrder.BIG_ENDIAN) {
dataOutputStream.writeShort(TIFF_BIG_ENDIAN)
} else {
dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN)
}
dataOutputStream.setByteOrder(exifData !!.byteOrder)
dataOutputStream.writeShort(TIFF_HEADER)
dataOutputStream.writeInt(8)
writeAllTags(dataOutputStream)
writeThumbnail(dataOutputStream)
for(t in nullTags) {
exifData !!.addTag(t)
}
dataOutputStream.flush()
}
private fun stripNullValueTags(data : ExifData) : ArrayList<ExifTag> {
val nullTags = ArrayList<ExifTag>()
for(t in data.allTags !!) {
if(t.getValue() == null && ! ExifInterface.isOffsetTag(t.tagId)) {
data.removeTag(t.tagId, t.ifd)
nullTags.add(t)
}
}
return nullTags
}
@Throws(IOException::class)
private fun writeThumbnail(dataOutputStream : OrderedDataOutputStream) {
if(exifData !!.hasCompressedThumbnail()) {
Log.d(TAG, "writing thumbnail..")
dataOutputStream.write(exifData !!.compressedThumbnail !!)
} else if(exifData !!.hasUncompressedStrip()) {
Log.d(TAG, "writing uncompressed strip..")
for(i in 0 until exifData !!.stripCount) {
dataOutputStream.write(exifData !!.getStrip(i) !!)
}
}
}
@Throws(IOException::class)
private fun writeAllTags(dataOutputStream : OrderedDataOutputStream) {
writeIfd(exifData !!.getIfdData(IfdId.TYPE_IFD_0) !!, dataOutputStream)
writeIfd(exifData !!.getIfdData(IfdId.TYPE_IFD_EXIF) !!, dataOutputStream)
val interoperabilityIfd = exifData !!.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY)
if(interoperabilityIfd != null) {
writeIfd(interoperabilityIfd, dataOutputStream)
}
val gpsIfd = exifData !!.getIfdData(IfdId.TYPE_IFD_GPS)
if(gpsIfd != null) {
writeIfd(gpsIfd, dataOutputStream)
}
val ifd1 = exifData !!.getIfdData(IfdId.TYPE_IFD_1)
if(ifd1 != null) {
writeIfd(exifData !!.getIfdData(IfdId.TYPE_IFD_1) !!, dataOutputStream)
}
}
@Throws(IOException::class)
private fun writeIfd(ifd : IfdData, dataOutputStream : OrderedDataOutputStream) {
val tags = ifd.allTags
dataOutputStream.writeShort(tags.size.toShort())
for(tag in tags) {
dataOutputStream.writeShort(tag.tagId)
dataOutputStream.writeShort(tag.dataType)
dataOutputStream.writeInt(tag.componentCount)
// Log.v( TAG, "\n" + tag.toString() );
if(tag.dataSize > 4) {
dataOutputStream.writeInt(tag.offset)
} else {
writeTagValue(tag, dataOutputStream)
var i = 0
val n = 4 - tag.dataSize
while(i < n) {
dataOutputStream.write(0)
i ++
}
}
}
dataOutputStream.writeInt(ifd.offsetToNextIfd)
for(tag in tags) {
if(tag.dataSize > 4) {
writeTagValue(tag, dataOutputStream)
}
}
}
private fun calculateOffsetOfIfd(ifd : IfdData, offsetArg : Int) : Int {
var offset = offsetArg
offset += 2 + ifd.tagCount * TAG_SIZE + 4
val tags = ifd.allTags
for(tag in tags) {
if(tag.dataSize > 4) {
tag.offset = offset
offset += tag.dataSize
}
}
return offset
}
@Throws(IOException::class)
private fun createRequiredIfdAndTag() {
// IFD0 is required for all file
var ifd0 = exifData !!.getIfdData(IfdId.TYPE_IFD_0)
if(ifd0 == null) {
ifd0 = IfdData(IfdId.TYPE_IFD_0)
exifData !!.addIfdData(ifd0)
}
val exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD)
?: throw IOException("No definition for crucial exif tag: " + ExifInterface.TAG_EXIF_IFD)
ifd0.setTag(exifOffsetTag)
// Exif IFD is required for all files.
var exifIfd = exifData !!.getIfdData(IfdId.TYPE_IFD_EXIF)
if(exifIfd == null) {
exifIfd = IfdData(IfdId.TYPE_IFD_EXIF)
exifData !!.addIfdData(exifIfd)
}
// GPS IFD
val gpsIfd = exifData !!.getIfdData(IfdId.TYPE_IFD_GPS)
if(gpsIfd != null) {
val gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD)
?: throw IOException("No definition for crucial exif tag: " + ExifInterface.TAG_GPS_IFD)
ifd0.setTag(gpsOffsetTag)
}
// Interoperability IFD
val interIfd = exifData !!.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY)
if(interIfd != null) {
val interOffsetTag =
mInterface.buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD)
?: throw IOException("No definition for crucial exif tag: " + ExifInterface.TAG_INTEROPERABILITY_IFD)
exifIfd.setTag(interOffsetTag)
}
var ifd1 = exifData !!.getIfdData(IfdId.TYPE_IFD_1)
// thumbnail
when {
exifData !!.hasCompressedThumbnail() -> {
if(ifd1 == null) {
ifd1 = IfdData(IfdId.TYPE_IFD_1)
exifData !!.addIfdData(ifd1)
}
val offsetTag =
mInterface.buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)
?: throw IOException("No definition for crucial exif tag: ${ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT}")
ifd1.setTag(offsetTag)
val lengthTag =
mInterface.buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)
?: throw IOException("No definition for crucial exif tag: ${ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH}")
lengthTag.setValue(exifData !!.compressedThumbnail !!.size)
ifd1.setTag(lengthTag)
// Get rid of tags for uncompressed if they exist.
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS))
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS))
}
exifData !!.hasUncompressedStrip() -> {
if(ifd1 == null) {
ifd1 = IfdData(IfdId.TYPE_IFD_1)
exifData !!.addIfdData(ifd1)
}
val stripCount = exifData !!.stripCount
val offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS)
?: throw IOException("No definition for crucial exif tag: ${ExifInterface.TAG_STRIP_OFFSETS}")
val lengthTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS)
?: throw IOException("No definition for crucial exif tag: ${ExifInterface.TAG_STRIP_BYTE_COUNTS}")
val lengths = LongArray(stripCount)
for(i in 0 until exifData !!.stripCount) {
lengths[i] = exifData !!.getStrip(i) !!.size.toLong()
}
lengthTag.setValue(lengths)
ifd1.setTag(offsetTag)
ifd1.setTag(lengthTag)
// Get rid of tags for compressed if they exist.
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH))
}
ifd1 != null -> {
// Get rid of offset and length tags if there is no thumbnail.
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS))
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS))
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH))
}
}
}
private fun calculateAllOffset() : Int {
var offset = TIFF_HEADER_SIZE.toInt()
val exifData = this.exifData !!
val ifd0 = exifData.getIfdData(IfdId.TYPE_IFD_0)
offset = calculateOffsetOfIfd(ifd0 !!, offset)
ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD))
?.setValue(offset)
val exifIfd = exifData.getIfdData(IfdId.TYPE_IFD_EXIF)
offset = calculateOffsetOfIfd(exifIfd !!, offset)
val interIfd = exifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY)
if(interIfd != null) {
exifIfd.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD))
?.setValue(offset)
offset = calculateOffsetOfIfd(interIfd, offset)
}
val gpsIfd = exifData.getIfdData(IfdId.TYPE_IFD_GPS)
if(gpsIfd != null) {
ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD))
?.setValue(offset)
offset = calculateOffsetOfIfd(gpsIfd, offset)
}
val ifd1 = exifData.getIfdData(IfdId.TYPE_IFD_1)
if(ifd1 != null) {
ifd0.offsetToNextIfd = offset
offset = calculateOffsetOfIfd(ifd1, offset)
}
// thumbnail
if(exifData .hasCompressedThumbnail()) {
ifd1 !!.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
?.setValue(offset)
offset += exifData .compressedThumbnail !!.size
} else if(exifData .hasUncompressedStrip()) {
val stripCount = exifData .stripCount
val offsets = LongArray(stripCount)
for(i in 0 until exifData .stripCount) {
offsets[i] = offset.toLong()
offset += exifData .getStrip(i) !!.size
}
ifd1 !!.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS))
?.setValue(offsets)
}
return offset
}
companion object {
private const val TAG = "ExifOutputStream"
private const val STREAMBUFFER_SIZE = 0x00010000 // 64Kb
private const val STATE_SOI = 0
private const val EXIF_HEADER = 0x45786966
private const val TIFF_HEADER : Short = 0x002A
private const val TIFF_BIG_ENDIAN : Short = 0x4d4d
private const val TIFF_LITTLE_ENDIAN : Short = 0x4949
private const val TAG_SIZE : Short = 12
private const val TIFF_HEADER_SIZE : Short = 8
private const val MAX_EXIF_SIZE = 65535
@Throws(IOException::class)
fun writeTagValue(tag : ExifTag, dataOutputStream : OrderedDataOutputStream) {
when(tag.dataType) {
ExifTag.TYPE_ASCII -> {
val buf = tag.stringByte !!
if(buf.size == tag.componentCount) {
buf[buf.size - 1] = 0
dataOutputStream.write(buf)
} else {
dataOutputStream.write(buf)
dataOutputStream.write(0)
}
}
ExifTag.TYPE_LONG, ExifTag.TYPE_UNSIGNED_LONG -> run {
for(i in 0 until tag.componentCount) {
dataOutputStream.writeInt(tag.getValueAt(i).toInt())
}
}
ExifTag.TYPE_RATIONAL, ExifTag.TYPE_UNSIGNED_RATIONAL -> run {
for(i in 0 until tag.componentCount) {
dataOutputStream.writeRational(tag.getRational(i) !!)
}
}
ExifTag.TYPE_UNDEFINED, ExifTag.TYPE_UNSIGNED_BYTE -> {
val buf = ByteArray(tag.componentCount)
tag.getBytes(buf)
dataOutputStream.write(buf)
}
ExifTag.TYPE_UNSIGNED_SHORT -> {
for(i in 0 until tag.componentCount) {
dataOutputStream.writeShort(tag.getValueAt(i).toShort())
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,116 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
/**
* This class reads the EXIF header of a JPEG file and stores it in
* {@link ExifData}.
*/
class ExifReader {
private static final String TAG = "ExifReader";
private final ExifInterface mInterface;
ExifReader( ExifInterface iRef ) {
mInterface = iRef;
}
/**
* Parses the inputStream and and returns the EXIF data in an
* {@link ExifData}.
*
* @throws ExifInvalidFormatException
* @throws java.io.IOException
*/
protected ExifData read( InputStream inputStream, int options ) throws ExifInvalidFormatException, IOException {
ExifParser parser = ExifParser.parse( inputStream, options, mInterface );
ExifData exifData = new ExifData( parser.getByteOrder() );
exifData.setSections( parser.getSections() );
exifData.mUncompressedDataPosition = parser.getUncompressedDataPosition();
exifData.setQualityGuess( parser.getQualityGuess() );
exifData.setJpegProcess( parser.getJpegProcess() );
final int w = parser.getImageWidth();
final int h = parser.getImageLength();
if( w > 0 && h > 0 ) {
exifData.setImageSize( w, h );
}
ExifTag tag;
int event = parser.next();
while( event != ExifParser.EVENT_END ) {
switch( event ) {
case ExifParser.EVENT_START_OF_IFD:
exifData.addIfdData( new IfdData( parser.getCurrentIfd() ) );
break;
case ExifParser.EVENT_NEW_TAG:
tag = parser.getTag();
if( ! tag.hasValue() ) {
parser.registerForTagValue( tag );
}
else {
// Log.v(TAG, "parsing id " + tag.getTagId() + " = " + tag);
if (parser.isDefinedTag(tag.getIfd(), tag.getTagId())) {
exifData.getIfdData(tag.getIfd()).setTag(tag);
}
else {
Log.w(TAG, "skip tag because not registered in the tag table:" + tag);
}
}
break;
case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
tag = parser.getTag();
if( tag.getDataType() == ExifTag.TYPE_UNDEFINED ) {
parser.readFullTagValue( tag );
}
exifData.getIfdData( tag.getIfd() ).setTag( tag );
break;
case ExifParser.EVENT_COMPRESSED_IMAGE:
byte buf[] = new byte[parser.getCompressedImageSize()];
if( buf.length == parser.read( buf ) ) {
exifData.setCompressedThumbnail( buf );
}
else {
Log.w( TAG, "Failed to read the compressed thumbnail" );
}
break;
case ExifParser.EVENT_UNCOMPRESSED_STRIP:
buf = new byte[parser.getStripSize()];
if( buf.length == parser.read( buf ) ) {
exifData.setStripBytes( parser.getStripIndex(), buf );
}
else {
Log.w( TAG, "Failed to read the strip bytes" );
}
break;
}
event = parser.next();
}
return exifData;
}
}

View File

@ -0,0 +1,112 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2
import android.util.Log
import java.io.IOException
import java.io.InputStream
/**
* This class reads the EXIF header of a JPEG file and stores it in
* [ExifData].
*/
internal class ExifReader(private val mInterface : ExifInterface) {
/**
* Parses the inputStream and and returns the EXIF data in an
* [ExifData].
*
* @throws ExifInvalidFormatException
* @throws java.io.IOException
*/
@Throws(ExifInvalidFormatException::class, IOException::class)
fun read(inputStream : InputStream, options : Int) : ExifData {
val parser = ExifParser.parse(inputStream, options, mInterface)
val exifData = ExifData(parser.byteOrder !!)
exifData.sections = parser.sections
exifData.mUncompressedDataPosition = parser.uncompressedDataPosition
exifData.qualityGuess = parser.qualityGuess
exifData.jpegProcess = parser.jpegProcess
val w = parser.imageWidth
val h = parser.imageLength
if(w > 0 && h > 0) {
exifData.setImageSize(w, h)
}
var tag : ExifTag?
var event = parser.next()
while(event != ExifParser.EVENT_END) {
when(event) {
ExifParser.EVENT_START_OF_IFD -> exifData.addIfdData(IfdData(parser.currentIfd))
ExifParser.EVENT_NEW_TAG -> {
tag = parser.tag
if(! tag !!.hasValue()) {
parser.registerForTagValue(tag)
} else {
// Log.v(TAG, "parsing id " + tag.getTagId() + " = " + tag);
if(parser.isDefinedTag(tag.ifd, tag.tagId.toInt())) {
exifData.getIfdData(tag.ifd) !!.setTag(tag)
} else {
Log.w(TAG, "skip tag because not registered in the tag table:$tag")
}
}
}
ExifParser.EVENT_VALUE_OF_REGISTERED_TAG -> {
tag = parser.tag
if(tag !!.dataType == ExifTag.TYPE_UNDEFINED) {
parser.readFullTagValue(tag)
}
exifData.getIfdData(tag.ifd) !!.setTag(tag)
}
ExifParser.EVENT_COMPRESSED_IMAGE -> {
val buf = ByteArray(parser.compressedImageSize)
if(buf.size == parser.read(buf)) {
exifData.compressedThumbnail = buf
} else {
Log.w(TAG, "Failed to read the compressed thumbnail")
}
}
ExifParser.EVENT_UNCOMPRESSED_STRIP -> {
val buf = ByteArray(parser.stripSize)
if(buf.size == parser.read(buf)) {
exifData.setStripBytes(parser.stripIndex, buf)
} else {
Log.w(TAG, "Failed to read the strip bytes")
}
}
}
event = parser.next()
}
return exifData
}
companion object {
private const val TAG = "ExifReader"
}
}

View File

@ -0,0 +1,893 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2
import java.nio.charset.Charset
import java.text.SimpleDateFormat
import java.util.*
/**
* This class stores information of an EXIF tag. For more information about
* defined EXIF tags, please read the Jeita EXIF 2.2 standard. Tags should be
* instantiated using [ExifInterface.buildTag].
*
* @see ExifInterface
*/
// Use builtTag in ExifInterface instead of constructor.
@Suppress("unused")
open class ExifTag internal constructor(
// Exif TagId. the TID of this tag.
val tagId : Short,
// Exif Tag Type. the data type of this tag
/*
* @see .TYPE_ASCII
* @see .TYPE_LONG
* @see .TYPE_RATIONAL
* @see .TYPE_UNDEFINED
* @see .TYPE_UNSIGNED_BYTE
* @see .TYPE_UNSIGNED_LONG
* @see .TYPE_UNSIGNED_RATIONAL
* @see .TYPE_UNSIGNED_SHORT
*/
val dataType : Short,
componentCount : Int,
// The ifd that this tag should be put in. the ID of the IFD this tag belongs to.
/*
* @see IfdId.TYPE_IFD_0
* @see IfdId.TYPE_IFD_1
* @see IfdId.TYPE_IFD_EXIF
* @see IfdId.TYPE_IFD_GPS
* @see IfdId.TYPE_IFD_INTEROPERABILITY
*/
var ifd : Int,
// If tag has defined count
private var mHasDefinedDefaultComponentCount : Boolean
) {
// Actual data count in tag (should be number of elements in value array)
/**
* Gets the component count of this tag.
*/
// TODO: fix integer overflows with this
var componentCount : Int = 0
private set
// The value (array of elements of type Tag Type)
private var mValue : Any? = null
// Value offset in exif header. the offset of this tag.
// This is only valid if this data size > 4 and contains an offset to the location of the actual value.
var offset : Int = 0
// the total data size in bytes of the value of this tag.
val dataSize : Int
get() = componentCount * getElementSize(dataType)
/**
* Gets the value as a byte array. This method should be used for tags of
* type [.TYPE_UNDEFINED] or [.TYPE_UNSIGNED_BYTE].
*
* @return the value as a byte array, or null if the tag's value does not
* exist or cannot be converted to a byte array.
*/
val valueAsBytes : ByteArray?
get() = mValue as? ByteArray
/**
* Gets the value as an array of longs. This method should be used for tags
* of type [.TYPE_UNSIGNED_LONG].
*
* @return the value as as an array of longs, or null if the tag's value
* does not exist or cannot be converted to an array of longs.
*/
val valueAsLongs : LongArray?
get() = mValue as? LongArray
/**
* Gets the value as an array of Rationals. This method should be used for
* tags of type [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL].
*
* @return the value as as an array of Rationals, or null if the tag's value
* does not exist or cannot be converted to an array of Rationals.
*/
@Suppress("UNCHECKED_CAST")
val valueAsRationals : Array<Rational>?
get() = mValue as? Array<Rational>
/**
* Gets the value as an array of ints. This method should be used for tags
* of type [.TYPE_UNSIGNED_SHORT], [.TYPE_UNSIGNED_LONG].
*
* @return the value as as an array of ints, or null if the tag's value does
* not exist or cannot be converted to an array of ints.
*/
// Truncates
val valueAsInts : IntArray?
get() = when(val v = mValue){
is LongArray-> IntArray(v.size){ v[it].toInt()}
else ->null
}
/**
* Gets the value as a String. This method should be used for tags of type
* [.TYPE_ASCII].
*
* @return the value as a String, or null if the tag's value does not exist
* or cannot be converted to a String.
*/
val valueAsString : String?
get() = when(val v = mValue) {
is String -> v
is ByteArray -> String(v, US_ASCII)
else -> null
}
/**
* Gets the [.TYPE_ASCII] data.
*
* @throws IllegalArgumentException If the type is NOT
* [.TYPE_ASCII].
*/
protected val string : String
get() = valueAsString !!
/*
* Get the converted ascii byte. Used by ExifOutputStream.
*/
val stringByte : ByteArray?
get() = mValue as? ByteArray
init {
this.componentCount = componentCount
mValue = null
}
/**
* Sets the component count of this tag. Call this function before
* setValue() if the length of value does not match the component count.
*/
fun forceSetComponentCount(count : Int) {
componentCount = count
}
/**
* Returns true if this ExifTag contains value; otherwise, this tag will
* contain an offset value that is determined when the tag is written.
*/
fun hasValue() : Boolean {
return mValue != null
}
/**
* Sets integer values into this tag. This method should be used for tags of
* type [.TYPE_UNSIGNED_SHORT]. This method will fail if:
*
* * The component type of this tag is not [.TYPE_UNSIGNED_SHORT],
* [.TYPE_UNSIGNED_LONG], or [.TYPE_LONG].
* * The value overflows.
* * The value.length does NOT match the component count in the definition
* for this tag.
*
*/
fun setValue(value : IntArray) : Boolean {
if(checkBadComponentCount(value.size)) {
return false
}
if(dataType != TYPE_UNSIGNED_SHORT && dataType != TYPE_LONG &&
dataType != TYPE_UNSIGNED_LONG) {
return false
}
if(dataType == TYPE_UNSIGNED_SHORT && checkOverflowForUnsignedShort(value)) {
return false
} else if(dataType == TYPE_UNSIGNED_LONG && checkOverflowForUnsignedLong(value)) {
return false
}
val data = LongArray(value.size)
for(i in value.indices) {
data[i] = value[i].toLong()
}
mValue = data
componentCount = value.size
return true
}
/**
* Sets integer value into this tag. This method should be used for tags of
* type [.TYPE_UNSIGNED_SHORT], or [.TYPE_LONG]. This method
* will fail if:
*
* * The component type of this tag is not [.TYPE_UNSIGNED_SHORT],
* [.TYPE_UNSIGNED_LONG], or [.TYPE_LONG].
* * The value overflows.
* * The component count in the definition of this tag is not 1.
*
*/
fun setValue(value : Int) : Boolean {
return setValue(intArrayOf(value))
}
/**
* Sets long values into this tag. This method should be used for tags of
* type [.TYPE_UNSIGNED_LONG]. This method will fail if:
*
* * The component type of this tag is not [.TYPE_UNSIGNED_LONG].
* * The value overflows.
* * The value.length does NOT match the component count in the definition
* for this tag.
*
*/
fun setValue(value : LongArray) : Boolean {
if(checkBadComponentCount(value.size) || dataType != TYPE_UNSIGNED_LONG) {
return false
}
if(checkOverflowForUnsignedLong(value)) {
return false
}
mValue = value
componentCount = value.size
return true
}
/**
* Sets long values into this tag. This method should be used for tags of
* type [.TYPE_UNSIGNED_LONG]. This method will fail if:
*
* * The component type of this tag is not [.TYPE_UNSIGNED_LONG].
* * The value overflows.
* * The component count in the definition for this tag is not 1.
*
*/
fun setValue(value : Long) : Boolean {
return setValue(longArrayOf(value))
}
/**
* Sets Rational values into this tag. This method should be used for tags
* of type [.TYPE_UNSIGNED_RATIONAL], or [.TYPE_RATIONAL]. This
* method will fail if:
*
* * The component type of this tag is not [.TYPE_UNSIGNED_RATIONAL]
* or [.TYPE_RATIONAL].
* * The value overflows.
* * The value.length does NOT match the component count in the definition
* for this tag.
*
*
* @see Rational
*/
fun setValue(value : Array<Rational>) : Boolean {
if(checkBadComponentCount(value.size)) {
return false
}
if(dataType != TYPE_UNSIGNED_RATIONAL && dataType != TYPE_RATIONAL) {
return false
}
if(dataType == TYPE_UNSIGNED_RATIONAL && checkOverflowForUnsignedRational(value)) {
return false
} else if(dataType == TYPE_RATIONAL && checkOverflowForRational(value)) {
return false
}
mValue = value
componentCount = value.size
return true
}
/**
* Sets a Rational value into this tag. This method should be used for tags
* of type [.TYPE_UNSIGNED_RATIONAL], or [.TYPE_RATIONAL]. This
* method will fail if:
*
* * The component type of this tag is not [.TYPE_UNSIGNED_RATIONAL]
* or [.TYPE_RATIONAL].
* * The value overflows.
* * The component count in the definition for this tag is not 1.
*
*
* @see Rational
*/
fun setValue(value : Rational) : Boolean =
setValue(arrayOf(value))
/**
* Sets byte values into this tag. This method should be used for tags of
* type [.TYPE_UNSIGNED_BYTE] or [.TYPE_UNDEFINED]. This method
* will fail if:
*
* * The component type of this tag is not [.TYPE_UNSIGNED_BYTE] or
* [.TYPE_UNDEFINED] .
* * The length does NOT match the component count in the definition for
* this tag.
*
*/
@JvmOverloads
fun setValue(value : ByteArray, offset : Int = 0, length : Int = value.size) : Boolean {
if(checkBadComponentCount(length)) {
return false
}
if(dataType != TYPE_UNSIGNED_BYTE && dataType != TYPE_UNDEFINED) {
return false
}
componentCount = length
mValue = ByteArray(length).also{
System.arraycopy(value, offset, it, 0, length)
}
return true
}
/**
* Sets byte value into this tag. This method should be used for tags of
* type [.TYPE_UNSIGNED_BYTE] or [.TYPE_UNDEFINED]. This method
* will fail if:
*
* * The component type of this tag is not [.TYPE_UNSIGNED_BYTE] or
* [.TYPE_UNDEFINED] .
* * The component count in the definition for this tag is not 1.
*
*/
fun setValue(value : Byte) : Boolean =
setValue(byteArrayOf(value))
/**
* Sets the value for this tag using an appropriate setValue method for the
* given object. This method will fail if:
*
* * The corresponding setValue method for the class of the object passed
* in would fail.
* * There is no obvious way to cast the object passed in into an EXIF tag
* type.
*
*/
inline fun <reified T : Any?> setValueAny(obj : T) : Boolean {
when(obj) {
null -> return false
is String -> return setValue(obj)
is ByteArray -> return setValue(obj)
is IntArray -> return setValue(obj)
is LongArray -> return setValue(obj)
is Rational -> return setValue(obj)
is Byte -> return setValue(obj.toByte())
is Short -> return setValue(obj.toInt() and 0x0ffff)
is Int -> return setValue(obj.toInt())
is Long -> return setValue(obj.toLong())
else ->{
@Suppress("UNCHECKED_CAST")
val ra = obj as? Array<Rational>
if(ra != null) return setValue( ra )
// Nulls in this array are treated as zeroes.
@Suppress("UNCHECKED_CAST")
val sa = obj as? Array<Short?>
if( sa != null) return setValue(IntArray(sa.size){ (sa[it]?.toInt() ?: 0) and 0xffff})
// Nulls in this array are treated as zeroes.
@Suppress("UNCHECKED_CAST")
val ia = obj as? Array<Int?>
if( ia != null) return setValue(IntArray(ia.size){ ia[it] ?: 0 })
// Nulls in this array are treated as zeroes.
@Suppress("UNCHECKED_CAST")
val la = obj as? Array<Long?>
if( la != null) return setValue(LongArray(la.size){ la[it] ?: 0L })
// Nulls in this array are treated as zeroes.
@Suppress("UNCHECKED_CAST")
val ba = obj as? Array<Byte?>
if( ba != null) return setValue(ByteArray(ba.size){ ba[it] ?: 0 })
return false
}
}
}
/**
* Sets a timestamp to this tag. The method converts the timestamp with the
* format of "yyyy:MM:dd kk:mm:ss" and calls [.setValue]. This
* method will fail if the data type is not [.TYPE_ASCII] or the
* component count of this tag is not 20 or undefined.
*
* @param time the number of milliseconds since Jan. 1, 1970 GMT
* @return true on success
*/
fun setValueTime(time : Long) : Boolean {
// synchronized on TIME_FORMAT as SimpleDateFormat is not thread safe
synchronized(TIME_FORMAT) {
return setValue(TIME_FORMAT.format(Date(time)))
}
}
/**
* Sets a string value into this tag. This method should be used for tags of
* type [.TYPE_ASCII]. The string is converted to an ASCII string.
* Characters that cannot be converted are replaced with '?'. The length of
* the string must be equal to either (component count -1) or (component
* count). The final byte will be set to the string null terminator '\0',
* overwriting the last character in the string if the value.length is equal
* to the component count. This method will fail if:
*
* * The data type is not [.TYPE_ASCII] or [.TYPE_UNDEFINED].
* * The length of the string is not equal to (component count -1) or
* (component count) in the definition for this tag.
*
*/
fun setValue(value : String) : Boolean {
if(dataType != TYPE_ASCII && dataType != TYPE_UNDEFINED) {
return false
}
val buf = value.toByteArray(US_ASCII)
val finalBuf = when {
buf.isNotEmpty() -> when {
buf[buf.size - 1].toInt() == 0 || dataType == TYPE_UNDEFINED -> buf
else -> buf.copyOf(buf.size + 1)
}
dataType == TYPE_ASCII && componentCount == 1 -> byteArrayOf(0)
else -> buf
}
val count = finalBuf.size
if(checkBadComponentCount(count)) {
return false
}
componentCount = count
mValue = finalBuf
return true
}
private fun checkBadComponentCount(count : Int) : Boolean {
return mHasDefinedDefaultComponentCount && componentCount != count
}
/**
* Gets the value as a String. This method should be used for tags of type
* [.TYPE_ASCII].
*
* @param defaultValue the String to return if the tag's value does not
* exist or cannot be converted to a String.
* @return the tag's value as a String, or the defaultValue.
*/
fun getValueAsString(defaultValue : String) : String {
return valueAsString ?: defaultValue
}
/**
* Gets the value as a byte. If there are more than 1 bytes in this value,
* gets the first byte. This method should be used for tags of type
* [.TYPE_UNDEFINED] or [.TYPE_UNSIGNED_BYTE].
*
* @param defaultValue the byte to return if tag's value does not exist or
* cannot be converted to a byte.
* @return the tag's value as a byte, or the defaultValue.
*/
fun getValueAsByte(defaultValue : Byte) : Byte {
val array = valueAsBytes
return if(array?.isNotEmpty()==true) array[0] else defaultValue
}
/**
* Gets the value as a Rational. If there are more than 1 Rationals in this
* value, gets the first one. This method should be used for tags of type
* [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL].
*
* @param defaultValue the numerator of the Rational to return if tag's
* value does not exist or cannot be converted to a Rational (the
* denominator will be 1).
* @return the tag's value as a Rational, or the defaultValue.
*/
fun getValueAsRational(defaultValue : Long) : Rational =
getValueAsRational( Rational(defaultValue, 1))
/**
* Gets the value as a Rational. If there are more than 1 Rationals in this
* value, gets the first one. This method should be used for tags of type
* [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL].
*
* @param defaultValue the Rational to return if tag's value does not exist
* or cannot be converted to a Rational.
* @return the tag's value as a Rational, or the defaultValue.
*/
private fun getValueAsRational(defaultValue : Rational) : Rational {
val array = valueAsRationals
return if(array?.isNotEmpty()==true) array[0] else defaultValue
}
/**
* Gets the value as an int. If there are more than 1 ints in this value,
* gets the first one. This method should be used for tags of type
* [.TYPE_UNSIGNED_SHORT], [.TYPE_UNSIGNED_LONG].
*
* @param defaultValue the int to return if tag's value does not exist or
* cannot be converted to an int.
* @return the tag's value as a int, or the defaultValue.
*/
fun getValueAsInt(defaultValue : Int) : Int {
val array = valueAsInts
return if(array?.isNotEmpty()==true) array[0] else defaultValue
}
/**
* Gets the value or null if none exists. If there are more than 1 longs in
* this value, gets the first one. This method should be used for tags of
* type [.TYPE_UNSIGNED_LONG].
*
* @param defaultValue the long to return if tag's value does not exist or
* cannot be converted to a long.
* @return the tag's value as a long, or the defaultValue.
*/
fun getValueAsLong(defaultValue : Long) : Long {
val array = valueAsLongs
return if(array?.isNotEmpty()==true) array[0] else defaultValue
}
/**
* Gets the tag's value or null if none exists.
*/
fun getValue() : Any? {
return mValue
}
/**
* Gets a long representation of the value.
*
* @param defaultValue value to return if there is no value or value is a
* rational with a denominator of 0.
* @return the tag's value as a long, or defaultValue if no representation
* exists.
*/
fun forceGetValueAsLong(defaultValue : Long) : Long {
when(val v = mValue) {
is LongArray -> if(v.isNotEmpty()) return v[0]
is ByteArray -> if(v.isNotEmpty()) return v[0].toLong()
else -> {
val r = valueAsRationals
if(r?.isNotEmpty() == true && r[0].denominator != 0L) {
return r[0].toDouble().toLong()
}
}
}
return defaultValue
}
/**
* Gets the value for type [.TYPE_ASCII], [.TYPE_LONG],
* [.TYPE_UNDEFINED], [.TYPE_UNSIGNED_BYTE],
* [.TYPE_UNSIGNED_LONG], or [.TYPE_UNSIGNED_SHORT]. For
* [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL], call
* [.getRational] instead.
*
* @throws IllegalArgumentException if the data type is
* [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL].
*/
fun getValueAt(index : Int) : Long {
return when(val v = mValue) {
is LongArray -> v[index]
is ByteArray -> v[index].toLong()
else -> error(
"Cannot get integer value from ${convertTypeToString(dataType)}"
)
}
}
/**
* Gets the [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL] data.
*
* @throws IllegalArgumentException If the type is NOT
* [.TYPE_RATIONAL] or [.TYPE_UNSIGNED_RATIONAL].
*/
fun getRational(index : Int) : Rational? {
require(! (dataType != TYPE_RATIONAL && dataType != TYPE_UNSIGNED_RATIONAL)) {
"Cannot get RATIONAL value from " + convertTypeToString(dataType)
}
return valueAsRationals?.get(index)
}
/**
* Gets the [.TYPE_UNDEFINED] or [.TYPE_UNSIGNED_BYTE] data.
*
* @param buf the byte array in which to store the bytes read.
* @param offset the initial position in buffer to store the bytes.
* @param length the maximum number of bytes to store in buffer. If length >
* component count, only the valid bytes will be stored.
* @throws IllegalArgumentException If the type is NOT
* [.TYPE_UNDEFINED] or [.TYPE_UNSIGNED_BYTE].
*/
@JvmOverloads
fun getBytes(buf : ByteArray, offset : Int = 0, length : Int = buf.size) {
require(! (dataType != TYPE_UNDEFINED && dataType != TYPE_UNSIGNED_BYTE)) {
"Cannot get BYTE value from " + convertTypeToString(
dataType
)
}
System.arraycopy(
mValue !!,
0,
buf,
offset,
if(length > componentCount) componentCount else length
)
}
var hasDefinedCount : Boolean
get() = mHasDefinedDefaultComponentCount
set(value){
mHasDefinedDefaultComponentCount = value
}
private fun checkOverflowForUnsignedShort(value : IntArray) : Boolean {
for(v in value) {
if(v > UNSIGNED_SHORT_MAX || v < 0) {
return true
}
}
return false
}
private fun checkOverflowForUnsignedLong(value : LongArray) : Boolean {
for(v in value) {
if(v < 0 || v > UNSIGNED_LONG_MAX) {
return true
}
}
return false
}
private fun checkOverflowForUnsignedLong(value : IntArray) : Boolean {
for(v in value) {
if(v < 0) {
return true
}
}
return false
}
private fun checkOverflowForUnsignedRational(value : Array<Rational>) : Boolean {
for(v in value) {
if(v.numerator < 0 || v.denominator < 0 || v.numerator > UNSIGNED_LONG_MAX || v.denominator > UNSIGNED_LONG_MAX) {
return true
}
}
return false
}
private fun checkOverflowForRational(value : Array<Rational>) : Boolean {
for(v in value) {
if(v.numerator < LONG_MIN || v.denominator < LONG_MIN || v.numerator > LONG_MAX || v.denominator > LONG_MAX) {
return true
}
}
return false
}
override fun hashCode() : Int {
var result = tagId.toInt()
result = 31 * result + dataType.toInt()
result = 31 * result + ifd
result = 31 * result + componentCount
result = 31 * result + offset
result = 31 * result + mHasDefinedDefaultComponentCount.hashCode()
result = 31 * result + (mValue?.hashCode() ?: 0)
return result
}
override fun equals(other : Any?) : Boolean {
if(other !is ExifTag) return false
if(other.tagId != this.tagId
|| other.componentCount != this.componentCount
|| other.dataType != this.dataType
) {
return false
}
val va = this.mValue
val vb = other.mValue
return when {
va == null -> vb == null
vb == null -> false
va is LongArray -> when(vb) {
is LongArray -> Arrays.equals(va, vb)
else -> false
}
va is ByteArray -> when(vb) {
is ByteArray -> Arrays.equals(va, vb)
else -> false
}
va is Array<*> && va.isArrayOf<Rational>() -> when {
vb is Array<*> && vb.isArrayOf<Rational>() -> Arrays.equals(va, vb)
else -> false
}
else -> va == vb
}
}
override fun toString() : String {
val strTagId = String.format("%04X", tagId)
return "tag id: $strTagId\nifd id: $ifd\ntype: ${convertTypeToString(dataType)}\ncount: $componentCount\noffset: $offset\nvalue: ${forceGetValueAsString()}\n"
}
/**
* Gets a string representation of the value.
*/
private fun forceGetValueAsString() : String {
when(val v = mValue) {
null -> return ""
is ByteArray -> return when(dataType) {
TYPE_ASCII -> String(v, US_ASCII)
else -> Arrays.toString(v)
}
is LongArray -> return when {
v.size == 1 -> v[0].toString()
else -> Arrays.toString(v)
}
is Array<*> -> return when {
v.size == 1 -> v[0]?.toString() ?: ""
else -> Arrays.toString(v)
}
else -> return v.toString()
}
}
companion object {
/**
* The BYTE type in the EXIF standard. An 8-bit unsigned integer.
*/
const val TYPE_UNSIGNED_BYTE : Short = 1
/**
* The ASCII type in the EXIF standard. An 8-bit byte containing one 7-bit
* ASCII code. The final byte is terminated with NULL.
*/
const val TYPE_ASCII : Short = 2
/**
* The SHORT type in the EXIF standard. A 16-bit (2-byte) unsigned integer
*/
const val TYPE_UNSIGNED_SHORT : Short = 3
/**
* The LONG type in the EXIF standard. A 32-bit (4-byte) unsigned integer
*/
const val TYPE_UNSIGNED_LONG : Short = 4
/**
* The RATIONAL type of EXIF standard. It consists of two LONGs. The first
* one is the numerator and the second one expresses the denominator.
*/
const val TYPE_UNSIGNED_RATIONAL : Short = 5
/**
* The UNDEFINED type in the EXIF standard. An 8-bit byte that can take any
* value depending on the field definition.
*/
const val TYPE_UNDEFINED : Short = 7
/**
* The SLONG type in the EXIF standard. A 32-bit (4-byte) signed integer
* (2's complement notation).
*/
const val TYPE_LONG : Short = 9
/**
* The SRATIONAL type of EXIF standard. It consists of two SLONGs. The first
* one is the numerator and the second one is the denominator.
*/
const val TYPE_RATIONAL : Short = 10
internal const val SIZE_UNDEFINED = 0
private val TYPE_TO_SIZE_MAP = IntArray(11)
private const val UNSIGNED_SHORT_MAX = 65535
private const val UNSIGNED_LONG_MAX = 4294967295L
private const val LONG_MAX = Integer.MAX_VALUE.toLong()
private const val LONG_MIN = Integer.MIN_VALUE.toLong()
init {
TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_BYTE.toInt()] = 1
TYPE_TO_SIZE_MAP[TYPE_ASCII.toInt()] = 1
TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_SHORT.toInt()] = 2
TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_LONG.toInt()] = 4
TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_RATIONAL.toInt()] = 8
TYPE_TO_SIZE_MAP[TYPE_UNDEFINED.toInt()] = 1
TYPE_TO_SIZE_MAP[TYPE_LONG.toInt()] = 4
TYPE_TO_SIZE_MAP[TYPE_RATIONAL.toInt()] = 8
}
private val TIME_FORMAT = SimpleDateFormat("yyyy:MM:dd kk:mm:ss", Locale.ENGLISH)
private val US_ASCII = Charset.forName("US-ASCII")
/**
* Returns true if the given IFD is a valid IFD.
*/
fun isValidIfd(ifdId : Int) : Boolean =
when(ifdId) {
IfdId.TYPE_IFD_0,
IfdId.TYPE_IFD_1,
IfdId.TYPE_IFD_EXIF,
IfdId.TYPE_IFD_INTEROPERABILITY,
IfdId.TYPE_IFD_GPS -> true
else -> false
}
/**
* Returns true if a given type is a valid tag type.
*/
fun isValidType(type : Short) : Boolean =
when(type) {
TYPE_UNSIGNED_BYTE,
TYPE_ASCII,
TYPE_UNSIGNED_SHORT,
TYPE_UNSIGNED_LONG,
TYPE_UNSIGNED_RATIONAL,
TYPE_UNDEFINED,
TYPE_LONG,
TYPE_RATIONAL -> true
else -> false
}
/**
* Gets the element size of the given data type in bytes.
*
* @see .TYPE_ASCII
* @see .TYPE_LONG
* @see .TYPE_RATIONAL
* @see .TYPE_UNDEFINED
* @see .TYPE_UNSIGNED_BYTE
* @see .TYPE_UNSIGNED_LONG
* @see .TYPE_UNSIGNED_RATIONAL
* @see .TYPE_UNSIGNED_SHORT
*/
fun getElementSize(type : Short) : Int = TYPE_TO_SIZE_MAP[type.toInt()]
private fun convertTypeToString(type : Short) : String =
when(type) {
TYPE_UNSIGNED_BYTE -> "UNSIGNED_BYTE"
TYPE_ASCII -> "ASCII"
TYPE_UNSIGNED_SHORT -> "UNSIGNED_SHORT"
TYPE_UNSIGNED_LONG -> "UNSIGNED_LONG"
TYPE_UNSIGNED_RATIONAL -> "UNSIGNED_RATIONAL"
TYPE_UNDEFINED -> "UNDEFINED"
TYPE_LONG -> "LONG"
TYPE_RATIONAL -> "RATIONAL"
else -> ""
}
}
}
/**
* Equivalent to setValue(value, 0, value.length).
*/
/**
* Equivalent to getBytes(buffer, 0, buffer.length).
*/

View File

@ -1,33 +0,0 @@
package it.sephiroth.android.library.exif2;
import java.text.DecimalFormat;
import java.text.NumberFormat;
/**
* Created by alessandro on 20/04/14.
*/
public class ExifUtil {
static final NumberFormat formatter = DecimalFormat.getInstance();
public static String processLensSpecifications( Rational[] values ) {
Rational min_focal = values[0];
Rational max_focal = values[1];
Rational min_f = values[2];
Rational max_f = values[3];
formatter.setMaximumFractionDigits(1);
StringBuilder sb = new StringBuilder();
sb.append( formatter.format( min_focal.toDouble() ) );
sb.append( "-" );
sb.append( formatter.format( max_focal.toDouble() ) );
sb.append( "mm f/" );
sb.append( formatter.format( min_f.toDouble() ) );
sb.append( "-" );
sb.append( formatter.format( max_f.toDouble() ) );
return sb.toString();
}
}

View File

@ -0,0 +1,32 @@
package it.sephiroth.android.library.exif2
import java.text.DecimalFormat
/**
* Created by alessandro on 20/04/14.
*/
object ExifUtil {
private val formatter = DecimalFormat.getInstance()
fun processLensSpecifications(values : Array<Rational>) : String {
val min_focal = values[0]
val max_focal = values[1]
val min_f = values[2]
val max_f = values[3]
formatter.maximumFractionDigits = 1
val sb = StringBuilder()
sb.append(formatter.format(min_focal.toDouble()))
sb.append("-")
sb.append(formatter.format(max_focal.toDouble()))
sb.append("mm f/")
sb.append(formatter.format(min_f.toDouble()))
sb.append("-")
sb.append(formatter.format(max_f.toDouble()))
return sb.toString()
}
}

View File

@ -1,150 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2;
import java.util.HashMap;
import java.util.Map;
/**
* This class stores all the tags in an IFD.
*
* @see ExifData
* @see ExifTag
*/
class IfdData {
private static final int[] sIfds = { IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1, IfdId.TYPE_IFD_EXIF, IfdId.TYPE_IFD_INTEROPERABILITY, IfdId.TYPE_IFD_GPS };
private final int mIfdId;
private final Map<Short, ExifTag> mExifTags = new HashMap<Short, ExifTag>();
private int mOffsetToNextIfd = 0;
/**
* Creates an IfdData with given IFD ID.
*
* @see IfdId#TYPE_IFD_0
* @see IfdId#TYPE_IFD_1
* @see IfdId#TYPE_IFD_EXIF
* @see IfdId#TYPE_IFD_GPS
* @see IfdId#TYPE_IFD_INTEROPERABILITY
*/
IfdData( int ifdId ) {
mIfdId = ifdId;
}
static protected int[] getIfds() {
return sIfds;
}
/**
* Gets the {@link ExifTag} with given tag id. Return null if there is no
* such tag.
*/
protected ExifTag getTag( short tagId ) {
return mExifTags.get( tagId );
}
/**
* Adds or replaces a {@link ExifTag}.
*/
protected ExifTag setTag( ExifTag tag ) {
tag.setIfd( mIfdId );
return mExifTags.put( tag.getTagId(), tag );
}
protected boolean checkCollision( short tagId ) {
return mExifTags.get( tagId ) != null;
}
/**
* Removes the tag of the given ID
*/
protected void removeTag( short tagId ) {
mExifTags.remove( tagId );
}
/**
* Gets the offset of next IFD.
*/
protected int getOffsetToNextIfd() {
return mOffsetToNextIfd;
}
/**
* Sets the offset of next IFD.
*/
protected void setOffsetToNextIfd( int offset ) {
mOffsetToNextIfd = offset;
}
/**
* Returns true if all tags in this two IFDs are equal. Note that tags of
* IFDs offset or thumbnail offset will be ignored.
*/
@Override
public boolean equals( Object obj ) {
if( this == obj ) {
return true;
}
if( obj == null ) {
return false;
}
if( obj instanceof IfdData ) {
IfdData data = (IfdData) obj;
if( data.getId() == mIfdId && data.getTagCount() == getTagCount() ) {
ExifTag[] tags = data.getAllTags();
for( ExifTag tag : tags ) {
if( ExifInterface.isOffsetTag( tag.getTagId() ) ) {
continue;
}
ExifTag tag2 = mExifTags.get( tag.getTagId() );
if( ! tag.equals( tag2 ) ) {
return false;
}
}
return true;
}
}
return false;
}
/**
* Gets the tags count in the IFD.
*/
protected int getTagCount() {
return mExifTags.size();
}
/**
* Gets the ID of this IFD.
*
* @see IfdId#TYPE_IFD_0
* @see IfdId#TYPE_IFD_1
* @see IfdId#TYPE_IFD_EXIF
* @see IfdId#TYPE_IFD_GPS
* @see IfdId#TYPE_IFD_INTEROPERABILITY
*/
protected int getId() {
return mIfdId;
}
/**
* Get a array the contains all {@link ExifTag} in this IFD.
*/
protected ExifTag[] getAllTags() {
return mExifTags.values().toArray( new ExifTag[mExifTags.size()] );
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2
import java.util.HashMap
/**
* The constants of the IFD ID defined in EXIF spec.
*/
object IfdId {
const val TYPE_IFD_0 = 0
const val TYPE_IFD_1 = 1
const val TYPE_IFD_EXIF = 2
const val TYPE_IFD_INTEROPERABILITY = 3
const val TYPE_IFD_GPS = 4
/* This is used in ExifData to allocate enough IfdData */
const val TYPE_IFD_COUNT = 5
}
// This class stores all the tags in an IFD.
// an IfdData with given IFD ID.
internal class IfdData(
// the ID of this IFD.
val id : Int
) {
companion object {
val ifds = intArrayOf(
IfdId.TYPE_IFD_0,
IfdId.TYPE_IFD_1,
IfdId.TYPE_IFD_EXIF,
IfdId.TYPE_IFD_INTEROPERABILITY,
IfdId.TYPE_IFD_GPS
)
}
private val mExifTags = HashMap<Short, ExifTag>()
// the offset of next IFD.
var offsetToNextIfd = 0
// the tags count in the IFD.
val tagCount : Int
get() = mExifTags.size
// a array the contains all [ExifTag] in this IFD.
val allTags : Array<ExifTag>
get() = mExifTags.values.toTypedArray()
// checkCollision
fun contains(tagId : Short) : Boolean {
return mExifTags[tagId] != null
}
// the [ExifTag] with given tag id.
// null if there is no such tag.
fun getTag(tagId : Short) : ExifTag? {
return mExifTags[tagId]
}
// Adds or replaces a [ExifTag].
fun setTag(tag : ExifTag) : ExifTag? {
tag.ifd = id
return mExifTags.put(tag.tagId, tag)
}
// Removes the tag of the given ID
fun removeTag(tagId : Short) {
mExifTags.remove(tagId)
}
/**
* Returns true if all tags in this two IFDs are equal. Note that tags of
* IFDs offset or thumbnail offset will be ignored.
*/
override fun equals(other : Any?) : Boolean {
if(other === null) return false
if(other === this) return true
if(other is IfdData) {
if(other.id == id && other.tagCount == tagCount) {
val tags = other.allTags
for(tag in tags) {
if(ExifInterface.isOffsetTag(tag.tagId)) {
continue
}
val tag2 = mExifTags[tag.tagId]
if(tag != tag2) {
return false
}
}
return true
}
}
return false
}
override fun hashCode() : Int {
var result = id
result = 31 * result + mExifTags.hashCode()
result = 31 * result + offsetToNextIfd
return result
}
}

View File

@ -1,109 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2;
class JpegHeader {
/** Start Of Image **/
public static final int TAG_SOI = 0xD8;
/** JFIF (JPEG File Interchange Format) */
public static final int TAG_M_JFIF = 0xE0;
/** EXIF table */
public static final int TAG_M_EXIF = 0xE1;
/** Product Information Comment */
public static final int TAG_M_COM = 0xFE;
/** Quantization Table */
public static final int TAG_M_DQT = 0xDB;
/** Start of frame */
public static final int TAG_M_SOF0 = 0xC0;
public static final int TAG_M_SOF1 = 0xC1;
public static final int TAG_M_SOF2 = 0xC2;
public static final int TAG_M_SOF3 = 0xC3;
public static final int TAG_M_DHT = 0xC4;
public static final int TAG_M_SOF5 = 0xC5;
public static final int TAG_M_SOF6 = 0xC6;
public static final int TAG_M_SOF7 = 0xC7;
public static final int TAG_M_SOF9 = 0xC9;
public static final int TAG_M_SOF10 = 0xCA;
public static final int TAG_M_SOF11 = 0xCB;
public static final int TAG_M_SOF13 = 0xCD;
public static final int TAG_M_SOF14 = 0xCE;
public static final int TAG_M_SOF15 = 0xCF;
/** Start Of Scan **/
public static final int TAG_M_SOS = 0xDA;
/** End of Image */
public static final int TAG_M_EOI = 0xD9;
public static final int TAG_M_IPTC = 0xED;
/** default JFIF Header bytes */
public static final byte JFIF_HEADER[] = {
(byte) 0xff, (byte) JpegHeader.TAG_M_JFIF,
0x00, 0x10, 'J', 'F', 'I', 'F',
0x00, 0x01, 0x01, 0x01, 0x01, 0x2C, 0x01,
0x2C, 0x00, 0x00
};
public static final short SOI = (short) 0xFFD8;
public static final short M_EXIF = (short) 0xFFE1;
public static final short M_JFIF = (short) 0xFFE0;
public static final short M_EOI = (short) 0xFFD9;
/**
* SOF (start of frame). All value between M_SOF0 and SOF15 is SOF marker except for M_DHT, JPG,
* and DAC marker.
*/
public static final short M_SOF0 = (short) 0xFFC0;
public static final short M_SOF1 = (short) 0xFFC1;
public static final short M_SOF2 = (short) 0xFFC2;
public static final short M_SOF3 = (short) 0xFFC3;
public static final short M_SOF5 = (short) 0xFFC5;
public static final short M_SOF6 = (short) 0xFFC6;
public static final short M_SOF7 = (short) 0xFFC7;
public static final short M_SOF9 = (short) 0xFFC9;
public static final short M_SOF10 = (short) 0xFFCA;
public static final short M_SOF11 = (short) 0xFFCB;
public static final short M_SOF13 = (short) 0xFFCD;
public static final short M_SOF14 = (short) 0xFFCE;
public static final short M_SOF15 = (short) 0xFFCF;
public static final short M_DHT = (short) 0xFFC4;
public static final short JPG = (short) 0xFFC8;
public static final short DAC = (short) 0xFFCC;
/** Define quantization table */
public static final short M_DQT = (short) 0xFFDB;
/** IPTC marker */
public static final short M_IPTC = (short) 0xFFED;
/** Start of scan (begins compressed data */
public static final short M_SOS = (short) 0xFFDA;
/** Comment section * */
public static final short M_COM = (short) 0xFFFE; // Comment section
public static final boolean isSofMarker( short marker ) {
return marker >= M_SOF0 && marker <= M_SOF15 && marker != M_DHT && marker != JPG && marker != DAC;
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2
@Suppress("unused", "MemberVisibilityCanBePrivate")
object JpegHeader {
/** Start Of Image */
const val TAG_SOI = 0xD8
/** JFIF (JPEG File Interchange Format) */
const val TAG_M_JFIF = 0xE0
/** EXIF table */
const val TAG_M_EXIF = 0xE1
/** Product Information Comment */
const val TAG_M_COM = 0xFE
/** Quantization Table */
const val TAG_M_DQT = 0xDB
/** Start of frame */
const val TAG_M_SOF0 = 0xC0
const val TAG_M_SOF1 = 0xC1
const val TAG_M_SOF2 = 0xC2
const val TAG_M_SOF3 = 0xC3
const val TAG_M_DHT = 0xC4
const val TAG_M_SOF5 = 0xC5
const val TAG_M_SOF6 = 0xC6
const val TAG_M_SOF7 = 0xC7
const val TAG_M_SOF9 = 0xC9
const val TAG_M_SOF10 = 0xCA
const val TAG_M_SOF11 = 0xCB
const val TAG_M_SOF13 = 0xCD
const val TAG_M_SOF14 = 0xCE
const val TAG_M_SOF15 = 0xCF
/** Start Of Scan */
const val TAG_M_SOS = 0xDA
/** End of Image */
const val TAG_M_EOI = 0xD9
const val TAG_M_IPTC = 0xED
/** default JFIF Header bytes */
val JFIF_HEADER = byteArrayOf(
0xff.toByte(),
TAG_M_JFIF.toByte(),
0x00,
0x10,
'J'.toByte(),
'F'.toByte(),
'I'.toByte(),
'F'.toByte(),
0x00,
0x01,
0x01,
0x01,
0x01,
0x2C,
0x01,
0x2C,
0x00,
0x00
)
const val SOI = 0xFFD8.toShort()
const val M_EXIF = 0xFFE1.toShort()
const val M_JFIF = 0xFFE0.toShort()
const val M_EOI = 0xFFD9.toShort()
/**
* SOF (start of frame). All value between M_SOF0 and SOF15 is SOF marker except for M_DHT, JPG,
* and DAC marker.
*/
const val M_SOF0 = 0xFFC0.toShort()
const val M_SOF1 = 0xFFC1.toShort()
const val M_SOF2 = 0xFFC2.toShort()
const val M_SOF3 = 0xFFC3.toShort()
const val M_SOF5 = 0xFFC5.toShort()
const val M_SOF6 = 0xFFC6.toShort()
const val M_SOF7 = 0xFFC7.toShort()
const val M_SOF9 = 0xFFC9.toShort()
const val M_SOF10 = 0xFFCA.toShort()
const val M_SOF11 = 0xFFCB.toShort()
const val M_SOF13 = 0xFFCD.toShort()
const val M_SOF14 = 0xFFCE.toShort()
const val M_SOF15 = 0xFFCF.toShort()
const val M_DHT = 0xFFC4.toShort()
const val JPG = 0xFFC8.toShort()
const val DAC = 0xFFCC.toShort()
/** Define quantization table */
const val M_DQT = 0xFFDB.toShort()
/** IPTC marker */
const val M_IPTC = 0xFFED.toShort()
/** Start of scan (begins compressed data */
const val M_SOS = 0xFFDA.toShort()
/** Comment section * */
const val M_COM = 0xFFFE.toShort() // Comment section
fun isSofMarker(marker : Short) : Boolean {
return marker >= M_SOF0 && marker <= M_SOF15 && marker != M_DHT && marker != JPG && marker != DAC
}
}

View File

@ -1,56 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
class OrderedDataOutputStream extends FilterOutputStream {
private final ByteBuffer mByteBuffer = ByteBuffer.allocate( 4 );
public OrderedDataOutputStream( OutputStream out ) {
super( out );
}
public OrderedDataOutputStream setByteOrder( ByteOrder order ) {
mByteBuffer.order( order );
return this;
}
public OrderedDataOutputStream writeShort( short value ) throws IOException {
mByteBuffer.rewind();
mByteBuffer.putShort( value );
out.write( mByteBuffer.array(), 0, 2 );
return this;
}
public OrderedDataOutputStream writeRational( Rational rational ) throws IOException {
writeInt( (int) rational.getNumerator() );
writeInt( (int) rational.getDenominator() );
return this;
}
public OrderedDataOutputStream writeInt( int value ) throws IOException {
mByteBuffer.rewind();
mByteBuffer.putInt( value );
out.write( mByteBuffer.array() );
return this;
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2
import java.io.FilterOutputStream
import java.io.IOException
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
internal class OrderedDataOutputStream(out : OutputStream) : FilterOutputStream(out) {
private val mByteBuffer = ByteBuffer.allocate(4)
fun setByteOrder(order : ByteOrder) : OrderedDataOutputStream {
mByteBuffer.order(order)
return this
}
@Throws(IOException::class)
fun writeShort(value : Short) : OrderedDataOutputStream {
mByteBuffer.rewind()
mByteBuffer.putShort(value)
out.write(mByteBuffer.array(), 0, 2)
return this
}
@Throws(IOException::class)
fun writeRational(rational : Rational) : OrderedDataOutputStream {
writeInt(rational.numerator.toInt())
writeInt(rational.denominator.toInt())
return this
}
@Throws(IOException::class)
fun writeInt(value : Int) : OrderedDataOutputStream {
mByteBuffer.rewind()
mByteBuffer.putInt(value)
out.write(mByteBuffer.array())
return this
}
}

View File

@ -1,88 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2;
/**
* The rational data type of EXIF tag. Contains a pair of longs representing the
* numerator and denominator of a Rational number.
*/
public class Rational {
private final long mNumerator;
private final long mDenominator;
/**
* Create a Rational with a given numerator and denominator.
*
* @param nominator
* @param denominator
*/
public Rational( long nominator, long denominator ) {
mNumerator = nominator;
mDenominator = denominator;
}
/**
* Create a copy of a Rational.
*/
public Rational( Rational r ) {
mNumerator = r.mNumerator;
mDenominator = r.mDenominator;
}
/**
* Gets the numerator of the rational.
*/
public long getNumerator() {
return mNumerator;
}
/**
* Gets the denominator of the rational
*/
public long getDenominator() {
return mDenominator;
}
/**
* Gets the rational value as type double. Will cause a divide-by-zero error
* if the denominator is 0.
*/
public double toDouble() {
return mNumerator / (double) mDenominator;
}
@Override
public boolean equals( Object obj ) {
if( obj == null ) {
return false;
}
if( this == obj ) {
return true;
}
if( obj instanceof Rational ) {
Rational data = (Rational) obj;
return mNumerator == data.mNumerator && mDenominator == data.mDenominator;
}
return false;
}
@Override
public String toString() {
return mNumerator + "/" + mDenominator;
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.sephiroth.android.library.exif2
/**
* The rational data type of EXIF tag. Contains a pair of longs representing the
* numerator and denominator of a Rational number.
*/
class Rational(
val numerator : Long=0,//the numerator of the rational.
val denominator : Long =1//the denominator of the rational
) {
// copy from a Rational.
constructor(r : Rational) :this(
numerator = r.numerator,
denominator = r.denominator
)
// Gets the rational value as type double.
// Will cause a divide-by-zero error if the denominator is 0.
fun toDouble() : Double = numerator.toDouble() / denominator.toDouble()
override fun toString() : String = "$numerator/$denominator"
override fun equals(other : Any?) : Boolean {
return when {
other === null -> false
other === this -> true
other is Rational -> numerator == other.numerator && denominator == other.denominator
else -> false
}
}
override fun hashCode() : Int =
31 * numerator.hashCode() + denominator.hashCode()
}