1
0
mirror of https://github.com/tateisu/SubwayTooter synced 2025-02-07 06:04:23 +01:00

添付メディアのアップロード順序を改善

This commit is contained in:
tateisu 2018-11-29 04:57:50 +09:00
parent fd22ad6874
commit e7620557da
11 changed files with 521 additions and 322 deletions

View File

@ -207,11 +207,11 @@ class ActAccountSetting
data.handleGetContentResult(contentResolver).firstOrNull()?.let{
addAttachment(
requestCode,
it.first,
if( it.second?.isNotEmpty() == true)
it.second
it.uri,
if( it.mimeType?.isNotEmpty() == true)
it.mimeType
else
contentResolver.getType(it.first)
contentResolver.getType(it.uri)
)
}
}

View File

@ -918,8 +918,8 @@ class ActAppSetting : AppCompatActivity()
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
if(resultCode == RESULT_OK && data != null && requestCode == REQUEST_CODE_TIMELINE_FONT) {
data.handleGetContentResult(contentResolver).firstOrNull()?.first?.let { uri ->
val file = saveTimelineFont(uri, "TimelineFont")
data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let {
val file = saveTimelineFont(it, "TimelineFont")
if(file != null) {
timeline_font = file.absolutePath
saveUIToData()
@ -927,8 +927,8 @@ class ActAppSetting : AppCompatActivity()
}
}
} else if(resultCode == RESULT_OK && data != null && requestCode == REQUEST_CODE_TIMELINE_FONT_BOLD) {
data.handleGetContentResult(contentResolver).firstOrNull()?.first?.let { uri ->
val file = saveTimelineFont(uri, "TimelineFontBold")
data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let {
val file = saveTimelineFont(it, "TimelineFontBold")
if(file != null) {
timeline_font_bold = file.absolutePath
saveUIToData()
@ -936,12 +936,8 @@ class ActAppSetting : AppCompatActivity()
}
}
} else if(resultCode == RESULT_OK && data != null && requestCode == REQUEST_CODE_APP_DATA_IMPORT) {
data.handleGetContentResult(contentResolver).firstOrNull()?.first?.let { uri ->
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
importAppData(false, uri)
data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let {
importAppData(false, it)
}
}
super.onActivityResult(requestCode, resultCode, data)

View File

@ -72,7 +72,6 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
private lateinit var tvSampleAcct : TextView
private lateinit var tvSampleContent : TextView
internal var loading_busy : Boolean = false
private var last_image_uri : String? = null
@ -108,9 +107,9 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
}
override fun onClick(v : View) {
val builder : ColorPickerDialog.Builder
when(v.id) {
R.id.btnHeaderBackgroundEdit -> {
@ -227,9 +226,8 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
if(requestCode == REQUEST_CODE_PICK_BACKGROUND && data != null && resultCode == RESULT_OK) {
data.handleGetContentResult(contentResolver).firstOrNull()?.let { pair ->
updateBackground(pair.first)
}
data.handleGetContentResult(contentResolver).firstOrNull()
?.uri?.let { updateBackground(it) }
}
}
@ -428,7 +426,7 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
loadImage(ivColumnBackground, column.column_bg_image)
tvSampleAcct.setTextColor( column.getAcctColor())
tvSampleAcct.setTextColor(column.getAcctColor())
tvSampleContent.setTextColor(column.getContentColor())
} finally {

View File

@ -9,6 +9,7 @@ import android.content.ContentValues
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.database.Cursor
import android.graphics.Bitmap
import android.net.Uri
import android.os.AsyncTask
@ -53,6 +54,8 @@ import org.json.JSONObject
import java.io.*
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callback {
@ -334,15 +337,12 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
}
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
if(requestCode == REQUEST_CODE_ATTACHMENT_OLD && resultCode == Activity.RESULT_OK) {
data?.handleGetContentResult(contentResolver)?.forEach {
addAttachment(it.first, it.second)
}
checkAttachments(data?.handleGetContentResult(contentResolver))
} else if(requestCode == REQUEST_CODE_ATTACHMENT && resultCode == Activity.RESULT_OK) {
data?.handleGetContentResult(contentResolver)?.forEach {
addAttachment(it.first, it.second)
}
checkAttachments(data?.handleGetContentResult(contentResolver))
} else if(requestCode == REQUEST_CODE_CAMERA) {
if(resultCode != Activity.RESULT_OK) {
@ -466,19 +466,16 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
this.attachment_list.clear()
try {
val array = sv.toJsonArray()
for(i in 0 until array.length()) {
sv.toJsonArray().forEach {
if(it !is JSONObject) return@forEach
try {
val a = parseItem(
::TootAttachment,
ServiceType.MASTODON,
array.optJSONObject(i)
)
if(a != null) attachment_list.add(PostAttachment(a))
val a = TootAttachment.decodeJson(it)
attachment_list.add(PostAttachment(a))
} catch(ex : Throwable) {
log.trace(ex)
}
}
attachment_list.sort()
} catch(ex : Throwable) {
log.trace(ex)
}
@ -518,13 +515,13 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
Intent.ACTION_VIEW -> {
val uri = sent_intent.data
val type = sent_intent.type
if(uri != null) addAttachment(uri, type)
if(uri != null) addAttachment(uri,type)
}
Intent.ACTION_SEND -> {
val uri = sent_intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
val type = sent_intent.type
if(uri != null) addAttachment(uri, type)
if(uri != null) addAttachment(uri,type)
}
Intent.ACTION_SEND_MULTIPLE -> {
@ -547,18 +544,18 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
if(sv != null && account != null) {
try {
val reply_status = TootParser(this@ActPost, account).status(sv.toJsonObject())
val isQuoterRenote = intent.getBooleanExtra(KEY_QUOTED_RENOTE, false)
if(reply_status != null) {
if(isQuoterRenote) {
cbQuoteRenote.isChecked = true
// 引用リートはCWやメンションを引き継がない
}else{
} else {
// CW をリプライ元に合わせる
if(reply_status.spoiler_text?.isNotEmpty() == true) {
cbContentWarning.isChecked = true
@ -661,6 +658,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
this.attachment_list.add(pa)
}
}
this.attachment_list.sort()
} catch(ex : Throwable) {
log.trace(ex)
}
@ -737,9 +735,8 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
override fun onDestroy() {
post_helper.onDestroy()
attachment_worker?.cancel()
super.onDestroy()
}
override fun onSaveInstanceState(outState : Bundle?) {
@ -767,10 +764,10 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
if(! attachment_list.isEmpty()) {
val array = JSONArray()
for(pa in attachment_list) {
if(pa.status == PostAttachment.STATUS_UPLOADED) {
// アップロード完了したものだけ保持する
array.put(pa.attachment?.json)
}
// アップロード完了したものだけ保持する
if(pa.status != PostAttachment.STATUS_UPLOADED) continue
val json = pa.attachment?.encodeJson() ?: continue
array.put(json)
}
outState.putString(KEY_ATTACHMENT_LIST, array.toString())
}
@ -1237,12 +1234,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
llAttachment.visibility = View.GONE
} else {
llAttachment.visibility = View.VISIBLE
var i = 0
val ie = ivMedia.size
while(i < ie) {
showAttachment_sub(ivMedia[i], i)
++ i
}
ivMedia.forEachIndexed { i,v ->showAttachment_sub(v, i) }
}
}
@ -1600,6 +1592,73 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
return null
}
private fun checkAttachments(srcList : ArrayList<GetContentResultEntry>?) {
srcList ?: return
fun Cursor.optLong(name : String) : Long? {
val idx = getColumnIndex(name)
val v = if(idx < 0) null else when(getType(idx)) {
Cursor.FIELD_TYPE_INTEGER -> getLong(idx)
Cursor.FIELD_TYPE_FLOAT -> getDouble(idx).toLong()
Cursor.FIELD_TYPE_STRING -> try {
getString(idx).toLong()
} catch(ex : Throwable) {
null
}
else -> null
}
return if(v == 0L) null else v
}
var countHasTime = 0
srcList.forEachIndexed { i, it ->
contentResolver.query(it.uri, null, null, null, null)?.use { cursor ->
if(! cursor.moveToNext()) return@forEachIndexed
for(name in cursor.columnNames.sorted()) {
val idx = cursor.getColumnIndex(name)
when(cursor.getType(idx)) {
Cursor.FIELD_TYPE_NULL -> log.d("[$i]$name=null")
Cursor.FIELD_TYPE_BLOB -> log.d("[$i]$name=(blob)")
Cursor.FIELD_TYPE_STRING -> log.d("[$i]$name=${cursor.getString(idx)}")
Cursor.FIELD_TYPE_INTEGER -> log.d("[$i]$name=(integer)${cursor.getLong(idx)}")
Cursor.FIELD_TYPE_FLOAT -> log.d("[$i]$name=(float)${cursor.getDouble(idx)}")
}
}
val t = cursor.optLong("date_modified")
?: cursor.optLong("last_modified")
?: cursor.optLong("date_added")
if(t != null) {
++ countHasTime
it.time = t
}
}
}
if( countHasTime == srcList.size ) {
srcList.sortBy { it.time }
}
srcList.forEach {
addAttachment(it.uri, it.mimeType)
}
}
class AttachmentRequest(
val account : SavedAccount,
val pa : PostAttachment,
val uri : Uri,
val mimeType : String,
val onUploadEnd : () -> Unit
)
val attachment_queue = ConcurrentLinkedQueue<AttachmentRequest>()
private var attachment_worker: AttachmentWorker? = null
var lastAttachmentAdd : Long = 0L
var lastAttachmentComplete : Long = 0L
@SuppressLint("StaticFieldLeak")
private fun addAttachment(
uri : Uri,
@ -1631,53 +1690,153 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
app_state.attachment_list = this.attachment_list
val pa = PostAttachment(this)
val pa = PostAttachment( this)
attachment_list.add(pa)
attachment_list.sort()
showMediaAttachment()
showToast(this, false, R.string.attachment_uploading)
// アップロード開始トースト(連発しない)
val now = System.currentTimeMillis()
if(now - lastAttachmentAdd >= 5000L) {
showToast(this, false, R.string.attachment_uploading)
}
lastAttachmentAdd = now
TootTaskRunner(this, TootTaskRunner.PROGRESS_NONE).run(account, object : TootTask {
override fun background(client : TootApiClient) : TootApiResult? {
if(mime_type.isEmpty()) {
return TootApiResult("mime_type is empty.")
// マストドンは添付メディアをID順に表示するため
// 画像が複数ある場合は一つずつ処理する必要がある
// 投稿画面ごとに1スレッドだけ作成してバックグラウンド処理を行う
attachment_queue.add(AttachmentRequest(account,pa,uri,mime_type,onUploadEnd))
val oldWorker = attachment_worker
if( oldWorker == null || !oldWorker.isAlive || oldWorker.isInterrupted ){
oldWorker?.cancel()
attachment_worker = AttachmentWorker().apply{ start() }
}else{
oldWorker.notifyEx()
}
}
inner class AttachmentWorker : WorkerBase(){
private val isCancelled = AtomicBoolean(false)
override fun cancel() {
isCancelled.set(true)
notifyEx()
}
override fun run() {
try {
while(! isCancelled.get()) {
val item = attachment_queue.poll()
if(item == null) {
waitEx(86400)
continue
}
val result = item.upload()
handler.post {
item.handleResult(result)
}
}
}catch(ex:Throwable){
log.trace(ex)
log.e(ex,"AttachmentWorker")
}
}
private fun AttachmentRequest.upload():TootApiResult?{
if(mimeType.isEmpty()) {
return TootApiResult("mime_type is empty.")
}
try {
val client = TootApiClient(this@ActPost,callback = object:TootApiCallback{
override val isApiCancelled : Boolean
get() = isCancelled.get()
})
client.account = account
val opener = createOpener(uri, mimeType)
val media_size_max = when {
mimeType.startsWith("video") -> {
1000000 * Math.max(1, Pref.spMovieSizeMax.toInt(pref))
}
else -> {
1000000 * Math.max(1, Pref.spMediaSizeMax.toInt(pref))
}
}
try {
val opener = createOpener(uri, mime_type)
val media_size_max = when {
mime_type.startsWith("video") -> {
1000000 * Math.max(1, Pref.spMovieSizeMax.toInt(pref))
}
else -> {
1000000 * Math.max(1, Pref.spMediaSizeMax.toInt(pref))
}
}
val content_length = getStreamSize(true, opener.open())
if(content_length > media_size_max) {
return TootApiResult(
getString(
R.string.file_size_too_big,
media_size_max / 1000000
)
val content_length = getStreamSize(true, opener.open())
if(content_length > media_size_max) {
return TootApiResult(
getString(
R.string.file_size_too_big,
media_size_max / 1000000
)
)
}
if(account.isMisskey) {
val multipart_builder = MultipartBody.Builder()
.setType(MultipartBody.FORM)
val apiKey = account.token_info?.parseString(TootApiClient.KEY_API_KEY_MISSKEY)
if(apiKey?.isNotEmpty() == true) {
multipart_builder.addFormDataPart("i", apiKey)
}
if(account.isMisskey) {
val multipart_builder = MultipartBody.Builder()
.setType(MultipartBody.FORM)
val apiKey =
account.token_info?.parseString(TootApiClient.KEY_API_KEY_MISSKEY)
if(apiKey?.isNotEmpty() == true) {
multipart_builder.addFormDataPart("i", apiKey)
multipart_builder.addFormDataPart(
"file", getDocumentName(contentResolver, uri), object : RequestBody() {
override fun contentType() : MediaType? {
return MediaType.parse(opener.mimeType)
}
@Throws(IOException::class)
override fun contentLength() : Long {
return content_length
}
@Throws(IOException::class)
override fun writeTo(sink : BufferedSink) {
opener.open().use { inData ->
val tmp = ByteArray(4096)
while(true) {
val r = inData.read(tmp, 0, tmp.size)
if(r <= 0) break
sink.write(tmp, 0, r)
}
}
}
}
multipart_builder.addFormDataPart(
"file", getDocumentName(contentResolver, uri), object : RequestBody() {
)
val request_builder = Request.Builder().post(multipart_builder.build())
val result = client.request("/api/drive/files/create", request_builder)
opener.deleteTempFile()
onUploadEnd()
val jsonObject = result?.jsonObject
if(jsonObject != null) {
val a = parseItem(::TootAttachment, ServiceType.MISSKEY, jsonObject)
if(a == null) {
result.error = "TootAttachment.parse failed"
} else {
pa.attachment = a
}
}
return result
} else {
val multipart_body = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart(
"file",
getDocumentName(contentResolver, uri),
object : RequestBody() {
override fun contentType() : MediaType? {
return MediaType.parse(opener.mimeType)
}
@ -1700,94 +1859,47 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
}
}
)
val request_builder = Request.Builder().post(multipart_builder.build())
val result = client.request("/api/drive/files/create", request_builder)
opener.deleteTempFile()
onUploadEnd()
val jsonObject = result?.jsonObject
if(jsonObject != null) {
val a = parseItem(::TootAttachment, ServiceType.MISSKEY, jsonObject)
if(a == null) {
result.error = "TootAttachment.parse failed"
} else {
pa.attachment = a
}
}
return result
} else {
val multipart_body = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart(
"file",
getDocumentName(contentResolver, uri),
object : RequestBody() {
override fun contentType() : MediaType? {
return MediaType.parse(opener.mimeType)
}
@Throws(IOException::class)
override fun contentLength() : Long {
return content_length
}
@Throws(IOException::class)
override fun writeTo(sink : BufferedSink) {
opener.open().use { inData ->
val tmp = ByteArray(4096)
while(true) {
val r = inData.read(tmp, 0, tmp.size)
if(r <= 0) break
sink.write(tmp, 0, r)
}
}
}
}
)
.build()
val request_builder = Request.Builder()
.post(multipart_body)
val result = client.request("/api/v1/media", request_builder)
opener.deleteTempFile()
onUploadEnd()
val jsonObject = result?.jsonObject
if(jsonObject != null) {
val a = parseItem(::TootAttachment, ServiceType.MASTODON, jsonObject)
if(a == null) {
result.error = "TootAttachment.parse failed"
} else {
pa.attachment = a
}
}
return result
}
.build()
} catch(ex : Throwable) {
return TootApiResult(ex.withCaption("read failed."))
val request_builder = Request.Builder()
.post(multipart_body)
val result = client.request("/api/v1/media", request_builder)
opener.deleteTempFile()
onUploadEnd()
val jsonObject = result?.jsonObject
if(jsonObject != null) {
val a = parseItem(::TootAttachment, ServiceType.MASTODON, jsonObject)
if(a == null) {
result.error = "TootAttachment.parse failed"
} else {
pa.attachment = a
}
}
return result
}
} catch(ex : Throwable) {
return TootApiResult(ex.withCaption("read failed."))
}
override fun handleResult(result : TootApiResult?) {
if(pa.attachment == null) {
pa.status = PostAttachment.STATUS_UPLOAD_FAILED
if(result != null) {
showToast(this@ActPost, true, result.error)
}
} else {
pa.status = PostAttachment.STATUS_UPLOADED
}
fun AttachmentRequest.handleResult(result : TootApiResult?) {
if(pa.attachment == null) {
pa.status = PostAttachment.STATUS_UPLOAD_FAILED
if(result != null) {
showToast(this@ActPost, true, result.error)
}
// 投稿中に画面回転があった場合、新しい画面のコールバックを呼び出す必要がある
pa.callback?.onPostAttachmentComplete(pa)
} else {
pa.status = PostAttachment.STATUS_UPLOADED
}
})
// 投稿中に画面回転があった場合、新しい画面のコールバックを呼び出す必要がある
pa.callback?.onPostAttachmentComplete(pa)
}
}
// 添付メディア投稿が完了したら呼ばれる
@ -1808,7 +1920,12 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
val a = pa.attachment
if(a != null) {
// アップロード完了
showToast(this@ActPost, false, R.string.attachment_uploaded)
val now = System.currentTimeMillis()
if(now - lastAttachmentComplete >= 5000L) {
showToast(this@ActPost, false, R.string.attachment_uploaded)
}
lastAttachmentComplete = now
if(Pref.bpAppendAttachmentUrlToContent(pref)) {
// 投稿欄の末尾に追記する
@ -2093,8 +2210,8 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
try {
val tmp_attachment_list = JSONArray()
for(pa in attachment_list) {
val a = pa.attachment
if(a != null) tmp_attachment_list.put(a.json)
val json = pa.attachment?.encodeJson()
if(json != null) tmp_attachment_list.put(json)
}
val json = JSONObject()
@ -2148,34 +2265,28 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
var content = draft.parseString(DRAFT_CONTENT) ?: ""
val account_db_id = draft.parseLong(DRAFT_ACCOUNT_DB_ID) ?: - 1L
var tmp_attachment_list = draft.optJSONArray(DRAFT_ATTACHMENT_LIST)
val tmp_attachment_list = draft.optJSONArray(DRAFT_ATTACHMENT_LIST)?.toObjectList()
val account = SavedAccount.loadAccount(this@ActPost, account_db_id)
if(account == null) {
list_warning.add(getString(R.string.account_in_draft_is_lost))
try {
var i = 0
val ie = tmp_attachment_list.length()
while(i < ie) {
val ta =
parseItem(
::TootAttachment,
ServiceType.MASTODON,
tmp_attachment_list.optJSONObject(i)
)
val text_url = ta?.text_url
if(text_url?.isNotEmpty() == true) {
content = content.replace(text_url, "")
if(tmp_attachment_list != null) {
// 本文からURLを除去する
tmp_attachment_list.forEach {
val text_url = TootAttachment.decodeJson(it).text_url
if(text_url?.isNotEmpty() == true) {
content = content.replace(text_url, "")
}
}
++ i
tmp_attachment_list.clear()
draft.put(DRAFT_ATTACHMENT_LIST, tmp_attachment_list.toJsonArray())
draft.put(DRAFT_CONTENT, content)
draft.remove(DRAFT_REPLY_ID)
draft.remove(DRAFT_REPLY_TEXT)
draft.remove(DRAFT_REPLY_IMAGE)
draft.remove(DRAFT_REPLY_URL)
}
tmp_attachment_list = JSONArray()
draft.put(DRAFT_ATTACHMENT_LIST, tmp_attachment_list)
draft.put(DRAFT_CONTENT, content)
draft.remove(DRAFT_REPLY_ID)
draft.remove(DRAFT_REPLY_TEXT)
draft.remove(DRAFT_REPLY_IMAGE)
draft.remove(DRAFT_REPLY_URL)
} catch(ignored : JSONException) {
}
@ -2208,30 +2319,27 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
}
}
try {
var isSomeAttachmentRemoved = false
for(i in tmp_attachment_list.length() - 1 downTo 0) {
if(isCancelled) return null
val ta = parseItem(
::TootAttachment,
ServiceType.MASTODON,
tmp_attachment_list.optJSONObject(i)
)
if(ta == null) {
if(tmp_attachment_list != null) {
// 添付メディアの存在確認
var isSomeAttachmentRemoved = false
val it = tmp_attachment_list.iterator()
while(it.hasNext()) {
if(isCancelled) return null
val ta = TootAttachment.decodeJson(it.next())
if(check_exist(ta.url)) continue
it.remove()
isSomeAttachmentRemoved = true
tmp_attachment_list.remove(i)
} else if(! check_exist(ta.url)) {
isSomeAttachmentRemoved = true
tmp_attachment_list.remove(i)
// 本文からURLを除去する
val text_url = ta.text_url
if(text_url?.isNotEmpty() == true) {
content = content.replace(text_url, "")
}
}
}
if(isSomeAttachmentRemoved) {
list_warning.add(getString(R.string.attachment_in_draft_is_lost))
draft.put(DRAFT_ATTACHMENT_LIST, tmp_attachment_list)
draft.put(DRAFT_CONTENT, content)
if(isSomeAttachmentRemoved) {
list_warning.add(getString(R.string.attachment_in_draft_is_lost))
draft.put(DRAFT_ATTACHMENT_LIST, tmp_attachment_list.toJsonArray())
draft.put(DRAFT_CONTENT, content)
}
}
} catch(ex : JSONException) {
log.trace(ex)
@ -2292,21 +2400,14 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
if(tmp_attachment_list.length() > 0) {
attachment_list.clear()
var i = 0
val ie = tmp_attachment_list.length()
while(i < ie) {
val ta = parseItem(
::TootAttachment,
ServiceType.MASTODON,
tmp_attachment_list.optJSONObject(i)
)
if(ta != null) {
val pa = PostAttachment(ta)
attachment_list.add(pa)
}
++ i
tmp_attachment_list.forEach {
if(it !is JSONObject) return@forEach
val pa = PostAttachment(TootAttachment.decodeJson(it))
attachment_list.add(pa)
}
attachment_list.sort()
}
if(reply_id != null) {
in_reply_to_id = reply_id
in_reply_to_text = reply_text
@ -2314,7 +2415,6 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
in_reply_to_url = reply_url
}
updateContentWarning()
showMediaAttachment()
showVisibility()

View File

@ -205,13 +205,12 @@ inline fun <reified T> parseListOrNull(
fun <T : TootAttachmentLike> ArrayList<T>.encodeJson() : JSONArray {
val a = JSONArray()
for(ta in this) {
if(ta is TootAttachment) {
try {
a.put(ta.json)
} catch(ex : JSONException) {
EntityUtil.log.e(ex, "encode failed.")
}
forEach { ta->
if(ta !is TootAttachment) return@forEach
try {
a.put(ta.encodeJson())
} catch(ex : JSONException) {
EntityUtil.log.e(ex, "encode failed.")
}
}
return a

View File

@ -27,7 +27,7 @@ open class TootAccount(parser : TootParser, src : JSONObject) {
// host, user ,(instance)
internal val reAccountUrl : Pattern =
Pattern.compile("\\Ahttps://([A-Za-z0-9._-]+)/@([A-Za-z0-9_]+)(?:@([A-Za-z0-9._-]+))?(?:\\z|[?#])")
Pattern.compile("""\Ahttps://([A-Za-z0-9._-]+)/@([A-Za-z0-9_][A-Za-z0-9_-]+)(?:@([A-Za-z0-9._-]+))?(?:\z|[?#])""")
fun getAcctFromUrl(url : String) : String? {
val m = reAccountUrl.matcher(url)

View File

@ -7,10 +7,11 @@ import org.json.JSONObject
import jp.juggler.subwaytooter.Pref
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.util.clipRange
import jp.juggler.subwaytooter.util.jsonObject
import jp.juggler.subwaytooter.util.parseLong
import jp.juggler.subwaytooter.util.parseString
class TootAttachment(serviceType:ServiceType,src : JSONObject) : TootAttachmentLike {
class TootAttachment : TootAttachmentLike {
companion object {
private fun parseFocusValue(parent : JSONObject?, key : String) : Float {
@ -19,13 +20,30 @@ class TootAttachment(serviceType:ServiceType,src : JSONObject) : TootAttachmentL
if(dv.isFinite()) return clipRange(- 1f, 1f, dv.toFloat())
}
return 0f
}
// 下書きからの復元などに使うパラメータ
// 後方互換性的な理由で概ねマストドンに合わせている
private const val KEY_IS_STRING_ID = "isStringId"
private const val KEY_ID = "id"
private const val KEY_TYPE = "type"
private const val KEY_URL = "url"
private const val KEY_REMOTE_URL = "remote_url"
private const val KEY_PREVIEW_URL = "preview_url"
private const val KEY_TEXT_URL = "text_url"
private const val KEY_DESCRIPTION = "description"
private const val KEY_IS_SENSITIVE = "isSensitive"
private const val KEY_META = "meta"
private const val KEY_FOCUS = "focus"
private const val KEY_X = "x"
private const val KEY_Y = "y"
private const val KEY_TIME_START_UPLOAD = "KEY_TIME_START_UPLOAD"
var timeSeed : Long = 0L
fun decodeJson(src : JSONObject) = TootAttachment(src, decode = true)
}
constructor(parser:TootParser,src:JSONObject):this(parser.serviceType,src)
val json : JSONObject
constructor(parser : TootParser, src : JSONObject) : this(parser.serviceType, src)
// ID of the attachment
val id : EntityId
@ -52,10 +70,12 @@ class TootAttachment(serviceType:ServiceType,src : JSONObject) : TootAttachmentL
override val focusY : Float
// 内部フラグ: 再編集で引き継いだ添付メディアなら真
var redraft :Boolean = false
var redraft : Boolean = false
// MisskeyはメディアごとにNSFWフラグがある
val isSensitive :Boolean
val isSensitive : Boolean
var timeStartUpload : Long = 0L
///////////////////////////////
@ -64,39 +84,40 @@ class TootAttachment(serviceType:ServiceType,src : JSONObject) : TootAttachmentL
else -> false
}
init {
json = src
constructor(serviceType : ServiceType, src : JSONObject) {
when(serviceType) {
ServiceType.MISSKEY -> {
id = EntityId.mayDefault( src.parseString("id"))
id = EntityId.mayDefault(src.parseString("id"))
val mimeType = src.parseString("type") ?: "?"
this.type = when{
mimeType.startsWith("image/") -> TootAttachmentLike.TYPE_IMAGE
mimeType.startsWith("video/") -> TootAttachmentLike.TYPE_VIDEO
mimeType.startsWith("audio/") -> TootAttachmentLike.TYPE_VIDEO
else-> TootAttachmentLike.TYPE_UNKNOWN
val mimeType = src.parseString("type") ?: "?"
this.type = when {
mimeType.startsWith("image/") -> TootAttachmentLike.TYPE_IMAGE
mimeType.startsWith("video/") -> TootAttachmentLike.TYPE_VIDEO
mimeType.startsWith("audio/") -> TootAttachmentLike.TYPE_VIDEO
else -> TootAttachmentLike.TYPE_UNKNOWN
}
url = src.parseString("url")
preview_url = src.parseString("thumbnailUrl")
remote_url = url
text_url = url
description = arrayOf(
src.parseString("name"),
src.parseString("comment")
)
.filterNotNull()
.joinToString(" / ")
focusX = 0f
focusY = 0f
isSensitive = src.optBoolean("isSensitive",false)
isSensitive = src.optBoolean("isSensitive", false)
}
else->{
id= EntityId.mayDefault(src.parseLong("id") )
else -> {
id = EntityId.mayDefault(src.parseLong("id"))
type = src.parseString("type")
url = src.parseString("url")
remote_url = src.parseString("remote_url")
@ -110,6 +131,10 @@ class TootAttachment(serviceType:ServiceType,src : JSONObject) : TootAttachmentL
focusY = parseFocusValue(focus, "y")
}
}
// internal use
// muse be > 0
this.timeStartUpload = src.parseLong(KEY_TIME_START_UPLOAD) ?: ++ timeSeed
}
override val urlForThumbnail : String?
@ -139,6 +164,52 @@ class TootAttachment(serviceType:ServiceType,src : JSONObject) : TootAttachmentL
}
return result
}
fun encodeJson() = jsonObject {
put(KEY_IS_STRING_ID, id is EntityIdString)
put(KEY_ID, id.toString())
put(KEY_TYPE, type)
put(KEY_URL, url)
put(KEY_REMOTE_URL, remote_url)
put(KEY_PREVIEW_URL, preview_url)
put(KEY_TEXT_URL, text_url)
put(KEY_DESCRIPTION, description)
put(KEY_IS_SENSITIVE, isSensitive)
put(KEY_TIME_START_UPLOAD, timeStartUpload)
if(focusX != 0f || focusY != 0f) {
put(KEY_META, jsonObject {
put(KEY_FOCUS, jsonObject {
put(KEY_X, focusX)
put(KEY_Y, focusY)
})
})
}
}
constructor(src : JSONObject, decode : Boolean) {
id = if( src.optBoolean(KEY_IS_STRING_ID) ) {
EntityId.mayDefault(src.parseString(KEY_ID))
} else {
EntityId.mayDefault(src.parseLong(KEY_ID))
}
type = src.parseString(KEY_TYPE)
url = src.parseString(KEY_URL)
remote_url = src.parseString(KEY_REMOTE_URL)
preview_url = src.parseString(KEY_PREVIEW_URL)
text_url = src.parseString(KEY_TEXT_URL)
description = src.parseString(KEY_DESCRIPTION)
isSensitive = src.optBoolean(KEY_IS_SENSITIVE)
val focus = src.optJSONObject(KEY_META)?.optJSONObject(KEY_FOCUS)
focusX = parseFocusValue(focus, KEY_X)
focusY = parseFocusValue(focus, KEY_Y)
timeStartUpload = src.parseLong(KEY_TIME_START_UPLOAD) ?: ++ timeSeed
}
}
// v1.3 から 添付ファイルの画像のピクセルサイズが取得できるようになった

View File

@ -56,11 +56,11 @@ fun createResizedBitmap(
}
val resize_to = Math.min(size, resizeToArg)
// inSampleSizeを計算
var bits = 0
var x = size
while(x > resize_to * 2) {
while(resize_to > 0 && x > resize_to * 2) {
++ bits
x = x shr 1
}
@ -121,7 +121,6 @@ fun createResizedBitmap(
6 -> {
tmp = dst_width
dst_width = dst_height
dst_height = tmp
matrix.postRotate(90f)

View File

@ -2,8 +2,8 @@ package jp.juggler.subwaytooter.util
import jp.juggler.subwaytooter.api.entity.TootAttachment
class PostAttachment {
class PostAttachment : Comparable<PostAttachment>{
companion object {
const val STATUS_UPLOADING = 1
const val STATUS_UPLOADED = 2
@ -16,10 +16,9 @@ class PostAttachment {
var status : Int
var attachment : TootAttachment? = null
var callback : Callback? = null
constructor(callback : Callback) {
this.status = STATUS_UPLOADING
this.callback = callback
@ -30,4 +29,13 @@ class PostAttachment {
this.attachment = a
}
override fun compareTo(other : PostAttachment) : Int {
val ta = this.attachment
val tb = other.attachment
return if(ta != null ){
if( tb == null) 1 else ta.id.compareTo(tb.id)
}else{
if( tb == null) 0 else -1
}
}
}

View File

@ -444,6 +444,8 @@ class PostHelper(
val a = pa.attachment ?: continue
if(a.redraft && ! instance.versionGE(TootInstance.VERSION_2_4_1)) continue
array.put(a.id)
log.d("media_ids id=${a.id} time=${pa.timeStartUpload}")
}
json.put("media_ids", array)
}

View File

@ -349,14 +349,14 @@ fun ByteArray.digestSHA256() : ByteArray {
}
fun ByteArray.startWith(
key:ByteArray,
thisOffset :Int =0,
keyOffset:Int=0,
length:Int = key.size-keyOffset
):Boolean{
if( this.size -thisOffset >= length && key.size -keyOffset >=length ){
for( i in 0 until length ){
if( this[i+thisOffset] != key[i+keyOffset]) return false
key : ByteArray,
thisOffset : Int = 0,
keyOffset : Int = 0,
length : Int = key.size - keyOffset
) : Boolean {
if(this.size - thisOffset >= length && key.size - keyOffset >= length) {
for(i in 0 until length) {
if(this[i + thisOffset] != key[i + keyOffset]) return false
}
return true
}
@ -364,24 +364,23 @@ fun ByteArray.startWith(
}
// 各要素の下位8ビットを使ってバイト配列を作る
fun IntArray.toByteArray():ByteArray{
fun IntArray.toByteArray() : ByteArray {
val dst = ByteArray(this.size)
for(i in 0 until this.size){
for(i in 0 until this.size) {
dst[i] = this[i].toByte()
}
return dst
}
// 各要素の下位8ビットを使ってバイト配列を作る
fun CharArray.toByteArray():ByteArray{
fun CharArray.toByteArray() : ByteArray {
val dst = ByteArray(this.size)
for(i in 0 until this.size){
for(i in 0 until this.size) {
dst[i] = this[i].toByte()
}
return dst
}
//// MD5ハッシュの作成
//@Suppress("unused")
//fun String.digestMD5() : String {
@ -398,9 +397,9 @@ fun String.digestSHA256Base64Url() : String {
return this.encodeUTF8().digestSHA256().encodeBase64Url()
}
fun String.toUri():Uri = Uri.parse(this)
fun String.toUri() : Uri = Uri.parse(this)
fun String.unescapeUri():String = Uri.decode(this)
fun String.unescapeUri() : String = Uri.decode(this)
////////////////////////////////////////////////////////////////////
// CharSequence
@ -485,10 +484,10 @@ fun String?.optInt() : Int? {
}
}
fun String?.filterNotEmpty() :String? = when{
this==null -> null
fun String?.filterNotEmpty() : String? = when {
this == null -> null
this.isEmpty() -> null
else->this
else -> this
}
//fun String.ellipsize(max : Int) = if(this.length > max) this.substring(0, max - 1) + "…" else this
@ -675,6 +674,24 @@ inline fun JSONArray.downForEachIndexed(block : (i : Int, v : Any?) -> Unit) {
}
}
fun JSONArray.toAnyList() : ArrayList<Any> {
val dst_list = ArrayList<Any>(length())
forEach { if(it != null) dst_list.add(it) }
return dst_list
}
fun JSONArray.toObjectList() : ArrayList<JSONObject> {
val dst_list = ArrayList<JSONObject>(length())
forEach { if(it is JSONObject) dst_list.add(it) }
return dst_list
}
fun List<JSONObject>.toJsonArray() : JSONArray {
val dst_list = JSONArray()
forEach { dst_list.put(it) }
return dst_list
}
fun JSONArray.toStringArrayList() : ArrayList<String> {
val dst_list = ArrayList<String>(length())
forEach { o ->
@ -706,6 +723,7 @@ fun JSONObject.parseFloatArrayList(name : String) : ArrayList<Float>? {
}
return null
}
fun String.toJsonObject() = JSONObject(this)
fun String.toJsonArray() = JSONArray(this)
@ -782,6 +800,12 @@ fun JSONObject.parseInt(key : String) : Int? {
fun JSONObject.toPostRequestBuilder() : Request.Builder =
Request.Builder().post(RequestBody.create(TootApiClient.MEDIA_TYPE_JSON, this.toString()))
fun jsonObject(initializer : JSONObject.() -> Unit) : JSONObject {
val dst = JSONObject()
dst.initializer()
return dst
}
////////////////////////////////////////////////////////////////////
// Bundle
@ -881,20 +905,19 @@ fun vg(v : View, visible : Boolean) {
v.visibility = if(visible) View.VISIBLE else View.GONE
}
fun Float.abs() :Float = Math.abs(this)
fun Float.abs() : Float = Math.abs(this)
////////////////////////////////////////////////////////////////////
// context
fun Context.loadRawResource( resId:Int):ByteArray{
resources.openRawResource(resId).use{ inStream->
val bao = ByteArrayOutputStream( inStream.available() )
IOUtils.copy(inStream,bao)
fun Context.loadRawResource(resId : Int) : ByteArray {
resources.openRawResource(resId).use { inStream ->
val bao = ByteArrayOutputStream(inStream.available())
IOUtils.copy(inStream, bao)
return bao.toByteArray()
}
}
////////////////////////////////////////////////////////////////////
// file
@ -936,17 +959,16 @@ fun showToast(context : Context, ex : Throwable, string_id : Int, vararg args :
Utils.showToastImpl(context, true, ex.withCaption(context.resources, string_id, *args))
}
fun Cursor.getInt(key:String) =
fun Cursor.getInt(key : String) =
getInt(getColumnIndex(key))
fun Cursor.getIntOrNull(idx:Int) =
fun Cursor.getIntOrNull(idx : Int) =
if(isNull(idx)) null else getInt(idx)
fun Cursor.getIntOrNull(key:String) =
fun Cursor.getIntOrNull(key : String) =
getIntOrNull(getColumnIndex(key))
fun Cursor.getLong(key:String) =
fun Cursor.getLong(key : String) =
getLong(getColumnIndex(key))
//fun Cursor.getLongOrNull(idx:Int) =
@ -955,18 +977,16 @@ fun Cursor.getLong(key:String) =
//fun Cursor.getLongOrNull(key:String) =
// getLongOrNull(getColumnIndex(key))
fun Cursor.getString(key:String) :String =
fun Cursor.getString(key : String) : String =
getString(getColumnIndex(key))
fun Cursor.getStringOrNull(keyIdx:Int) =
fun Cursor.getStringOrNull(keyIdx : Int) =
if(isNull(keyIdx)) null else getString(keyIdx)
fun Cursor.getStringOrNull(key:String) =
fun Cursor.getStringOrNull(key : String) =
getStringOrNull(getColumnIndex(key))
fun getDocumentName(contentResolver:ContentResolver,uri : Uri) : String {
fun getDocumentName(contentResolver : ContentResolver, uri : Uri) : String {
val errorName = "no_name"
return contentResolver.query(uri, null, null, null, null, null)
?.use { cursor ->
@ -995,7 +1015,7 @@ fun getStreamSize(bClose : Boolean, inStream : InputStream) : Long {
}
}
fun intentOpenDocument(mimeType : String):Intent{
fun intentOpenDocument(mimeType : String) : Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = mimeType // "image/*"
@ -1018,63 +1038,69 @@ fun intentGetContent(
// EXTRA_MIME_TYPES は API 19以降。ACTION_GET_CONTENT でも ACTION_OPEN_DOCUMENT でも指定できる
intent.putExtra("android.intent.extra.MIME_TYPES", mimeTypes)
intent.type =when {
mimeTypes.size == 1 -> mimeTypes[0]
intent.type = when {
mimeTypes.size == 1 -> mimeTypes[0]
// On Android 6.0 and above using "video/* image/" or "image/ video/*" type doesn't work
// it only recognizes the first filter you specify.
Build.VERSION.SDK_INT >= 23 -> "*/*"
else -> mimeTypes.joinToString(" ")
}
return Intent.createChooser(intent, caption)
}
data class GetContentResultEntry(
val uri:Uri,
val mimeType:String? =null,
var time : Long? =null
)
// returns list of pair of uri and mime-type.
fun Intent.handleGetContentResult(contentResolver : ContentResolver) : ArrayList<Pair<Uri, String?>> {
val urlList = ArrayList<Pair<Uri, String?>>()
fun Intent.handleGetContentResult(contentResolver : ContentResolver) : ArrayList<GetContentResultEntry> {
val urlList = ArrayList<GetContentResultEntry>()
// 単一選択
this.data?.let {
urlList.add(Pair(it, this.type))
urlList.add(GetContentResultEntry(it, this.type))
}
// 複数選択
val cd = this.clipData
if(cd != null) {
for(i in 0 until cd.itemCount) {
cd.getItemAt(i)?.uri?.let { uri ->
if(null == urlList.find { it.first == uri }) {
urlList.add(Pair(uri, null as String?))
if(null == urlList.find { it.uri == uri }) {
urlList.add(GetContentResultEntry(uri ))
}
}
}
}
urlList.forEach {
try{
try {
contentResolver.takePersistableUriPermission(
it.first,
it.uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}catch(_:Throwable){
} catch(_ : Throwable) {
}
}
return urlList
}
fun Matcher.groupOrNull( g:Int):String? =
if( groupCount() >= g ){
fun Matcher.groupOrNull(g : Int) : String? =
if(groupCount() >= g) {
group(g)
}else {
} else {
null
}
// colorARGB.applyAlphaMultiplier(0.5f) でalpha値が半分になったARGB値を得る
fun Int.applyAlphaMultiplier(alphaMultiplier: Float? = null):Int{
return if( alphaMultiplier ==null ){
fun Int.applyAlphaMultiplier(alphaMultiplier : Float? = null) : Int {
return if(alphaMultiplier == null) {
this
}else{
} else {
val rgb = (this and 0xffffff)
val alpha = clipRange(0,255,((this ushr 24).toFloat() * alphaMultiplier +0.5f ).toInt())
val alpha = clipRange(0, 255, ((this ushr 24).toFloat() * alphaMultiplier + 0.5f).toInt())
return rgb or (alpha shl 24)
}
}