1733 lines
65 KiB
Kotlin
1733 lines
65 KiB
Kotlin
package jp.juggler.subwaytooter
|
|
|
|
import android.Manifest
|
|
import android.app.Activity
|
|
import android.content.ContentValues
|
|
import android.content.Intent
|
|
import android.content.SharedPreferences
|
|
import android.content.pm.PackageManager
|
|
import android.graphics.Bitmap
|
|
import android.media.RingtoneManager
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.Handler
|
|
import android.provider.MediaStore
|
|
import android.text.Editable
|
|
import android.text.SpannableString
|
|
import android.text.TextWatcher
|
|
import android.view.View
|
|
import android.widget.*
|
|
import androidx.appcompat.app.AlertDialog
|
|
import androidx.appcompat.widget.SwitchCompat
|
|
import androidx.core.app.ActivityCompat
|
|
import androidx.core.content.ContextCompat
|
|
import jp.juggler.subwaytooter.Styler.defaultColorIcon
|
|
import jp.juggler.subwaytooter.api.*
|
|
import jp.juggler.subwaytooter.api.entity.*
|
|
import jp.juggler.subwaytooter.dialog.ActionsDialog
|
|
import jp.juggler.subwaytooter.notification.NotificationHelper
|
|
import jp.juggler.subwaytooter.notification.PollingWorker
|
|
import jp.juggler.subwaytooter.table.AcctColor
|
|
import jp.juggler.subwaytooter.table.SavedAccount
|
|
import jp.juggler.subwaytooter.util.*
|
|
import jp.juggler.subwaytooter.view.MyNetworkImageView
|
|
import jp.juggler.util.*
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.GlobalScope
|
|
import kotlinx.coroutines.launch
|
|
import okhttp3.MediaType
|
|
import okhttp3.MediaType.Companion.toMediaType
|
|
import okhttp3.MultipartBody
|
|
import okhttp3.RequestBody
|
|
import okio.BufferedSink
|
|
import org.jetbrains.anko.backgroundColor
|
|
import org.jetbrains.anko.textColor
|
|
import ru.gildor.coroutines.okhttp.await
|
|
import java.io.*
|
|
import kotlin.math.max
|
|
|
|
class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|
CompoundButton.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
|
|
|
companion object {
|
|
|
|
internal val log = LogCategory("ActAccountSetting")
|
|
|
|
internal const val KEY_ACCOUNT_DB_ID = "account_db_id"
|
|
|
|
internal const val REQUEST_CODE_ACCT_CUSTOMIZE = 1
|
|
internal const val REQUEST_CODE_NOTIFICATION_SOUND = 2
|
|
private const val REQUEST_CODE_AVATAR_ATTACHMENT = 3
|
|
private const val REQUEST_CODE_HEADER_ATTACHMENT = 4
|
|
private const val REQUEST_CODE_AVATAR_CAMERA = 5
|
|
private const val REQUEST_CODE_HEADER_CAMERA = 6
|
|
|
|
internal const val RESULT_INPUT_ACCESS_TOKEN = Activity.RESULT_FIRST_USER + 10
|
|
internal const val EXTRA_DB_ID = "db_id"
|
|
|
|
internal const val max_length_display_name = 30
|
|
internal const val max_length_note = 160
|
|
internal const val max_length_fields = 255
|
|
|
|
private const val PERMISSION_REQUEST_AVATAR = 1
|
|
private const val PERMISSION_REQUEST_HEADER = 2
|
|
|
|
internal const val MIME_TYPE_JPEG = "image/jpeg"
|
|
internal const val MIME_TYPE_PNG = "image/png"
|
|
|
|
fun open(activity: Activity, ai: SavedAccount, requestCode: Int) {
|
|
val intent = Intent(activity, ActAccountSetting::class.java)
|
|
intent.putExtra(KEY_ACCOUNT_DB_ID, ai.db_id)
|
|
activity.startActivityForResult(intent, requestCode)
|
|
}
|
|
|
|
}
|
|
|
|
internal lateinit var account: SavedAccount
|
|
internal lateinit var pref: SharedPreferences
|
|
|
|
private lateinit var tvInstance: TextView
|
|
private lateinit var tvUser: TextView
|
|
private lateinit var btnAccessToken: Button
|
|
private lateinit var btnInputAccessToken: Button
|
|
private lateinit var btnAccountRemove: Button
|
|
private lateinit var btnLoadPreference: Button
|
|
|
|
private lateinit var btnVisibility: Button
|
|
|
|
private lateinit var swNSFWOpen: SwitchCompat
|
|
private lateinit var swDontShowTimeout: SwitchCompat
|
|
private lateinit var swExpandCW: SwitchCompat
|
|
private lateinit var swMarkSensitive: SwitchCompat
|
|
|
|
private lateinit var btnOpenBrowser: Button
|
|
private lateinit var btnPushSubscription: Button
|
|
private lateinit var btnPushSubscriptionNotForce: Button
|
|
private lateinit var btnResetNotificationTracking: Button
|
|
|
|
|
|
private lateinit var cbNotificationMention: CheckBox
|
|
private lateinit var cbNotificationBoost: CheckBox
|
|
private lateinit var cbNotificationFavourite: CheckBox
|
|
private lateinit var cbNotificationFollow: CheckBox
|
|
private lateinit var cbNotificationFollowRequest: CheckBox
|
|
private lateinit var cbNotificationReaction: CheckBox
|
|
private lateinit var cbNotificationVote: CheckBox
|
|
private lateinit var cbNotificationPost: CheckBox
|
|
|
|
private lateinit var cbConfirmFollow: CheckBox
|
|
private lateinit var cbConfirmFollowLockedUser: CheckBox
|
|
private lateinit var cbConfirmUnfollow: CheckBox
|
|
private lateinit var cbConfirmBoost: CheckBox
|
|
private lateinit var cbConfirmFavourite: CheckBox
|
|
private lateinit var cbConfirmUnboost: CheckBox
|
|
private lateinit var cbConfirmUnfavourite: CheckBox
|
|
private lateinit var cbConfirmToot: CheckBox
|
|
|
|
private lateinit var tvUserCustom: TextView
|
|
private lateinit var btnUserCustom: View
|
|
|
|
private lateinit var btnNotificationSoundEdit: Button
|
|
private lateinit var btnNotificationSoundReset: Button
|
|
private lateinit var btnNotificationStyleEdit: Button
|
|
private lateinit var btnNotificationStyleEditReply: Button
|
|
|
|
private var notification_sound_uri: String? = null
|
|
|
|
private lateinit var ivProfileHeader: MyNetworkImageView
|
|
private lateinit var ivProfileAvatar: MyNetworkImageView
|
|
private lateinit var btnProfileAvatar: View
|
|
private lateinit var btnProfileHeader: View
|
|
private lateinit var etDisplayName: EditText
|
|
private lateinit var btnDisplayName: View
|
|
private lateinit var etNote: EditText
|
|
private lateinit var cbLocked: CheckBox
|
|
private lateinit var btnNote: View
|
|
private lateinit var etDefaultText: EditText
|
|
|
|
private lateinit var name_invalidator: NetworkEmojiInvalidator
|
|
private lateinit var note_invalidator: NetworkEmojiInvalidator
|
|
private lateinit var default_text_invalidator: NetworkEmojiInvalidator
|
|
internal lateinit var handler: Handler
|
|
|
|
internal var loading = false
|
|
|
|
private lateinit var listEtFieldName: List<EditText>
|
|
private lateinit var listEtFieldValue: List<EditText>
|
|
private lateinit var listFieldNameInvalidator: List<NetworkEmojiInvalidator>
|
|
private lateinit var listFieldValueInvalidator: List<NetworkEmojiInvalidator>
|
|
private lateinit var btnFields: View
|
|
|
|
private lateinit var etMaxTootChars: EditText
|
|
|
|
private lateinit var etMediaSizeMax: EditText
|
|
|
|
private lateinit var etMovieSizeMax: EditText
|
|
|
|
private lateinit var spResizeImage: Spinner
|
|
|
|
private lateinit var spPushPolicy: Spinner
|
|
|
|
private class ResizeItem(val config: ResizeConfig, val caption: String)
|
|
|
|
private lateinit var imageResizeItems: List<ResizeItem>
|
|
|
|
private class PushPolicyItem(val id: String?, val caption: String)
|
|
|
|
private lateinit var pushPolicyItems: List<PushPolicyItem>
|
|
|
|
///////////////////////////////////////////////////
|
|
|
|
internal var visibility = TootVisibility.Public
|
|
|
|
private var uriCameraImage: Uri? = null
|
|
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
App1.setActivityTheme(this)
|
|
this.pref = App1.pref
|
|
|
|
initUI()
|
|
|
|
val a = SavedAccount.loadAccount(this, intent.getLongExtra(KEY_ACCOUNT_DB_ID, -1L))
|
|
if (a == null) {
|
|
finish()
|
|
return
|
|
}
|
|
|
|
|
|
loadUIFromData(a)
|
|
|
|
initializeProfile()
|
|
|
|
btnOpenBrowser.text = getString(R.string.open_instance_website, account.apiHost.pretty)
|
|
}
|
|
|
|
override fun onStop() {
|
|
PollingWorker.queueUpdateNotification(this)
|
|
super.onStop()
|
|
}
|
|
|
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
when (requestCode) {
|
|
REQUEST_CODE_ACCT_CUSTOMIZE -> {
|
|
if (resultCode == Activity.RESULT_OK) {
|
|
showAcctColor()
|
|
}
|
|
}
|
|
|
|
REQUEST_CODE_NOTIFICATION_SOUND -> {
|
|
if (resultCode == Activity.RESULT_OK) {
|
|
// RINGTONE_PICKERからの選択されたデータを取得する
|
|
val uri = data?.extras?.get(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
|
if (uri is Uri) {
|
|
notification_sound_uri = uri.toString()
|
|
saveUIToData()
|
|
// Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), uri);
|
|
// TextView ringView = (TextView) findViewById(R.id.ringtone);
|
|
// ringView.setText(ringtone.getTitle(getApplicationContext()));
|
|
// ringtone.setStreamType(AudioManager.STREAM_ALARM);
|
|
// ringtone.play();
|
|
// SystemClock.sleep(1000);
|
|
// ringtone.stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
REQUEST_CODE_AVATAR_ATTACHMENT, REQUEST_CODE_HEADER_ATTACHMENT -> {
|
|
|
|
if (resultCode == Activity.RESULT_OK && data != null) {
|
|
data.handleGetContentResult(contentResolver).firstOrNull()?.let {
|
|
addAttachment(
|
|
requestCode,
|
|
it.uri,
|
|
it.mimeType?.notEmpty() ?: contentResolver.getType(it.uri)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
REQUEST_CODE_AVATAR_CAMERA, REQUEST_CODE_HEADER_CAMERA -> {
|
|
|
|
if (resultCode != Activity.RESULT_OK) {
|
|
// 失敗したら DBからデータを削除
|
|
val uriCameraImage = this@ActAccountSetting.uriCameraImage
|
|
if (uriCameraImage != null) {
|
|
contentResolver.delete(uriCameraImage, null, null)
|
|
this@ActAccountSetting.uriCameraImage = null
|
|
}
|
|
} else {
|
|
// 画像のURL
|
|
val uri = data?.data ?: uriCameraImage
|
|
if (uri != null) {
|
|
val type = contentResolver.getType(uri)
|
|
addAttachment(requestCode, uri, type)
|
|
}
|
|
}
|
|
}
|
|
|
|
else -> super.onActivityResult(requestCode, resultCode, data)
|
|
}
|
|
}
|
|
|
|
var density: Float = 1f
|
|
|
|
private fun initUI() {
|
|
this.density = resources.displayMetrics.density
|
|
this.handler = App1.getAppState(this).handler
|
|
setContentView(R.layout.act_account_setting)
|
|
App1.initEdgeToEdge(this)
|
|
|
|
val root: View = findViewById(R.id.svContent)
|
|
|
|
Styler.fixHorizontalPadding(root)
|
|
|
|
setSwitchColor(pref, root)
|
|
|
|
tvInstance = findViewById(R.id.tvInstance)
|
|
tvUser = findViewById(R.id.tvUser)
|
|
btnAccessToken = findViewById(R.id.btnAccessToken)
|
|
btnInputAccessToken = findViewById(R.id.btnInputAccessToken)
|
|
btnAccountRemove = findViewById(R.id.btnAccountRemove)
|
|
btnLoadPreference = findViewById(R.id.btnLoadPreference)
|
|
btnVisibility = findViewById(R.id.btnVisibility)
|
|
swNSFWOpen = findViewById(R.id.swNSFWOpen)
|
|
swDontShowTimeout = findViewById(R.id.swDontShowTimeout)
|
|
swExpandCW = findViewById(R.id.swExpandCW)
|
|
swMarkSensitive = findViewById(R.id.swMarkSensitive)
|
|
btnOpenBrowser = findViewById(R.id.btnOpenBrowser)
|
|
btnPushSubscription = findViewById(R.id.btnPushSubscription)
|
|
btnPushSubscriptionNotForce = findViewById(R.id.btnPushSubscriptionNotForce)
|
|
btnPushSubscriptionNotForce.vg(BuildConfig.DEBUG)
|
|
btnResetNotificationTracking = findViewById(R.id.btnResetNotificationTracking)
|
|
|
|
cbNotificationMention = findViewById(R.id.cbNotificationMention)
|
|
cbNotificationBoost = findViewById(R.id.cbNotificationBoost)
|
|
cbNotificationFavourite = findViewById(R.id.cbNotificationFavourite)
|
|
cbNotificationFollow = findViewById(R.id.cbNotificationFollow)
|
|
cbNotificationFollowRequest = findViewById(R.id.cbNotificationFollowRequest)
|
|
|
|
cbNotificationReaction = findViewById(R.id.cbNotificationReaction)
|
|
cbNotificationVote = findViewById(R.id.cbNotificationVote)
|
|
cbNotificationPost = findViewById(R.id.cbNotificationPost)
|
|
|
|
cbConfirmFollow = findViewById(R.id.cbConfirmFollow)
|
|
cbConfirmFollowLockedUser = findViewById(R.id.cbConfirmFollowLockedUser)
|
|
cbConfirmUnfollow = findViewById(R.id.cbConfirmUnfollow)
|
|
cbConfirmBoost = findViewById(R.id.cbConfirmBoost)
|
|
cbConfirmFavourite = findViewById(R.id.cbConfirmFavourite)
|
|
cbConfirmUnboost = findViewById(R.id.cbConfirmUnboost)
|
|
cbConfirmUnfavourite = findViewById(R.id.cbConfirmUnfavourite)
|
|
cbConfirmToot = findViewById(R.id.cbConfirmToot)
|
|
|
|
tvUserCustom = findViewById(R.id.tvUserCustom)
|
|
btnUserCustom = findViewById(R.id.btnUserCustom)
|
|
|
|
ivProfileHeader = findViewById(R.id.ivProfileHeader)
|
|
ivProfileAvatar = findViewById(R.id.ivProfileAvatar)
|
|
btnProfileAvatar = findViewById(R.id.btnProfileAvatar)
|
|
btnProfileHeader = findViewById(R.id.btnProfileHeader)
|
|
etDisplayName = findViewById(R.id.etDisplayName)
|
|
etDefaultText = findViewById(R.id.etDefaultText)
|
|
etMaxTootChars = findViewById(R.id.etMaxTootChars)
|
|
btnDisplayName = findViewById(R.id.btnDisplayName)
|
|
etNote = findViewById(R.id.etNote)
|
|
btnNote = findViewById(R.id.btnNote)
|
|
cbLocked = findViewById(R.id.cbLocked)
|
|
|
|
|
|
etMediaSizeMax = findViewById(R.id.etMediaSizeMax)
|
|
etMovieSizeMax = findViewById(R.id.etMovieSizeMax)
|
|
spResizeImage = findViewById(R.id.spResizeImage)
|
|
spPushPolicy = findViewById(R.id.spPushPolicy)
|
|
|
|
imageResizeItems = SavedAccount.resizeConfigList.map {
|
|
val caption = when (it.type) {
|
|
ResizeType.None -> getString(R.string.dont_resize)
|
|
ResizeType.LongSide -> getString(R.string.long_side_pixel, it.size)
|
|
ResizeType.SquarePixel -> if (it.extraStringId != 0) {
|
|
getString(
|
|
R.string.resize_square_pixels_2,
|
|
it.size * it.size,
|
|
getString(it.extraStringId)
|
|
)
|
|
} else {
|
|
getString(
|
|
R.string.resize_square_pixels,
|
|
it.size * it.size,
|
|
it.size
|
|
)
|
|
}
|
|
}
|
|
ResizeItem(it, caption)
|
|
}
|
|
spResizeImage.adapter = ArrayAdapter(
|
|
this,
|
|
android.R.layout.simple_spinner_item,
|
|
imageResizeItems.map { it.caption }.toTypedArray()
|
|
).apply {
|
|
setDropDownViewResource(R.layout.lv_spinner_dropdown)
|
|
}
|
|
|
|
pushPolicyItems = listOf(
|
|
PushPolicyItem(null, getString(R.string.unspecified)),
|
|
PushPolicyItem("all", getString(R.string.all)),
|
|
PushPolicyItem("followed", getString(R.string.following)),
|
|
PushPolicyItem("follower", getString(R.string.followers)),
|
|
PushPolicyItem("none", getString(R.string.no_one)),
|
|
)
|
|
|
|
spPushPolicy.adapter = ArrayAdapter(
|
|
this,
|
|
android.R.layout.simple_spinner_item,
|
|
pushPolicyItems.map { it.caption }.toTypedArray()
|
|
).apply {
|
|
setDropDownViewResource(R.layout.lv_spinner_dropdown)
|
|
}
|
|
|
|
listEtFieldName = arrayOf(
|
|
R.id.etFieldName1,
|
|
R.id.etFieldName2,
|
|
R.id.etFieldName3,
|
|
R.id.etFieldName4
|
|
).map { findViewById(it) }
|
|
|
|
listEtFieldValue = arrayOf(
|
|
R.id.etFieldValue1,
|
|
R.id.etFieldValue2,
|
|
R.id.etFieldValue3,
|
|
R.id.etFieldValue4
|
|
).map { findViewById(it) }
|
|
|
|
btnFields = findViewById(R.id.btnFields)
|
|
|
|
|
|
|
|
|
|
btnOpenBrowser.setOnClickListener(this)
|
|
btnPushSubscription.setOnClickListener(this)
|
|
btnPushSubscriptionNotForce.setOnClickListener(this)
|
|
btnResetNotificationTracking.setOnClickListener(this)
|
|
btnAccessToken.setOnClickListener(this)
|
|
btnInputAccessToken.setOnClickListener(this)
|
|
btnAccountRemove.setOnClickListener(this)
|
|
btnLoadPreference.setOnClickListener(this)
|
|
btnVisibility.setOnClickListener(this)
|
|
btnUserCustom.setOnClickListener(this)
|
|
btnProfileAvatar.setOnClickListener(this)
|
|
btnProfileHeader.setOnClickListener(this)
|
|
btnDisplayName.setOnClickListener(this)
|
|
btnNote.setOnClickListener(this)
|
|
btnFields.setOnClickListener(this)
|
|
|
|
swNSFWOpen.setOnCheckedChangeListener(this)
|
|
swDontShowTimeout.setOnCheckedChangeListener(this)
|
|
swExpandCW.setOnCheckedChangeListener(this)
|
|
swMarkSensitive.setOnCheckedChangeListener(this)
|
|
cbNotificationMention.setOnCheckedChangeListener(this)
|
|
cbNotificationBoost.setOnCheckedChangeListener(this)
|
|
cbNotificationFavourite.setOnCheckedChangeListener(this)
|
|
cbNotificationFollow.setOnCheckedChangeListener(this)
|
|
cbNotificationFollowRequest.setOnCheckedChangeListener(this)
|
|
cbNotificationReaction.setOnCheckedChangeListener(this)
|
|
cbNotificationVote.setOnCheckedChangeListener(this)
|
|
cbNotificationPost.setOnCheckedChangeListener(this)
|
|
|
|
cbLocked.setOnCheckedChangeListener(this)
|
|
|
|
|
|
cbConfirmFollow.setOnCheckedChangeListener(this)
|
|
cbConfirmFollowLockedUser.setOnCheckedChangeListener(this)
|
|
cbConfirmUnfollow.setOnCheckedChangeListener(this)
|
|
cbConfirmBoost.setOnCheckedChangeListener(this)
|
|
cbConfirmFavourite.setOnCheckedChangeListener(this)
|
|
cbConfirmUnboost.setOnCheckedChangeListener(this)
|
|
cbConfirmUnfavourite.setOnCheckedChangeListener(this)
|
|
cbConfirmToot.setOnCheckedChangeListener(this)
|
|
|
|
btnNotificationSoundEdit = findViewById(R.id.btnNotificationSoundEdit)
|
|
btnNotificationSoundReset = findViewById(R.id.btnNotificationSoundReset)
|
|
btnNotificationSoundEdit.setOnClickListener(this)
|
|
btnNotificationSoundReset.setOnClickListener(this)
|
|
|
|
btnNotificationStyleEdit = findViewById(R.id.btnNotificationStyleEdit)
|
|
btnNotificationStyleEditReply = findViewById(R.id.btnNotificationStyleEditReply)
|
|
btnNotificationStyleEdit.setOnClickListener(this)
|
|
btnNotificationStyleEditReply.setOnClickListener(this)
|
|
|
|
|
|
spResizeImage.onItemSelectedListener = this
|
|
|
|
spPushPolicy.onItemSelectedListener = this
|
|
|
|
btnNotificationStyleEditReply.vg(Pref.bpSeparateReplyNotificationGroup(pref))
|
|
|
|
name_invalidator = NetworkEmojiInvalidator(handler, etDisplayName)
|
|
note_invalidator = NetworkEmojiInvalidator(handler, etNote)
|
|
default_text_invalidator = NetworkEmojiInvalidator(handler, etDefaultText)
|
|
|
|
listFieldNameInvalidator = listEtFieldName.map {
|
|
NetworkEmojiInvalidator(handler, it)
|
|
}
|
|
|
|
listFieldValueInvalidator = listEtFieldValue.map {
|
|
NetworkEmojiInvalidator(handler, it)
|
|
}
|
|
|
|
etDefaultText.addTextChangedListener(object : TextWatcher {
|
|
override fun afterTextChanged(s: Editable?) {
|
|
saveUIToData()
|
|
}
|
|
|
|
override fun beforeTextChanged(
|
|
s: CharSequence?,
|
|
start: Int,
|
|
count: Int,
|
|
after: Int
|
|
) {
|
|
}
|
|
|
|
override fun onTextChanged(
|
|
s: CharSequence?,
|
|
start: Int,
|
|
before: Int,
|
|
count: Int
|
|
) {
|
|
}
|
|
})
|
|
|
|
etMaxTootChars.addTextChangedListener(object : TextWatcher {
|
|
override fun beforeTextChanged(
|
|
s: CharSequence?,
|
|
start: Int,
|
|
count: Int,
|
|
after: Int
|
|
) {
|
|
}
|
|
|
|
override fun onTextChanged(
|
|
s: CharSequence?,
|
|
start: Int,
|
|
before: Int,
|
|
count: Int
|
|
) {
|
|
}
|
|
|
|
override fun afterTextChanged(s: Editable?) {
|
|
val num = etMaxTootChars.parseInt()
|
|
if (num != null && num >= 0) {
|
|
saveUIToData()
|
|
}
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
private fun EditText.parseInt(): Int? {
|
|
val sv = this.text?.toString() ?: return null
|
|
return try {
|
|
Integer.parseInt(sv, 10)
|
|
} catch (ex: Throwable) {
|
|
null
|
|
}
|
|
}
|
|
|
|
private fun loadUIFromData(a: SavedAccount) {
|
|
this.account = a
|
|
|
|
tvInstance.text = a.apiHost.pretty
|
|
tvUser.text = a.acct.pretty
|
|
|
|
this.visibility = a.visibility
|
|
|
|
loading = true
|
|
|
|
swNSFWOpen.isChecked = a.dont_hide_nsfw
|
|
swDontShowTimeout.isChecked = a.dont_show_timeout
|
|
swExpandCW.isChecked = a.expand_cw
|
|
swMarkSensitive.isChecked = a.default_sensitive
|
|
cbNotificationMention.isChecked = a.notification_mention
|
|
cbNotificationBoost.isChecked = a.notification_boost
|
|
cbNotificationFavourite.isChecked = a.notification_favourite
|
|
cbNotificationFollow.isChecked = a.notification_follow
|
|
cbNotificationFollowRequest.isChecked = a.notification_follow_request
|
|
cbNotificationReaction.isChecked = a.notification_reaction
|
|
cbNotificationVote.isChecked = a.notification_vote
|
|
cbNotificationPost.isChecked = a.notification_post
|
|
|
|
cbConfirmFollow.isChecked = a.confirm_follow
|
|
cbConfirmFollowLockedUser.isChecked = a.confirm_follow_locked
|
|
cbConfirmUnfollow.isChecked = a.confirm_unfollow
|
|
cbConfirmBoost.isChecked = a.confirm_boost
|
|
cbConfirmFavourite.isChecked = a.confirm_favourite
|
|
cbConfirmUnboost.isChecked = a.confirm_unboost
|
|
cbConfirmUnfavourite.isChecked = a.confirm_unfavourite
|
|
|
|
|
|
cbConfirmToot.isChecked = a.confirm_post
|
|
|
|
notification_sound_uri = a.sound_uri
|
|
|
|
etDefaultText.setText(a.default_text)
|
|
etMaxTootChars.setText(a.max_toot_chars.toString())
|
|
|
|
loading = false
|
|
|
|
val enabled = !a.isPseudo
|
|
btnAccessToken.isEnabled = enabled
|
|
btnInputAccessToken.isEnabled = enabled
|
|
btnVisibility.isEnabled = enabled
|
|
btnPushSubscription.isEnabled = enabled
|
|
btnPushSubscriptionNotForce.isEnabled = enabled
|
|
btnResetNotificationTracking.isEnabled = enabled
|
|
btnNotificationSoundEdit.isEnabled = Build.VERSION.SDK_INT < 26 && enabled
|
|
btnNotificationSoundReset.isEnabled = Build.VERSION.SDK_INT < 26 && enabled
|
|
btnNotificationStyleEdit.isEnabled = Build.VERSION.SDK_INT >= 26 && enabled
|
|
btnNotificationStyleEditReply.isEnabled = Build.VERSION.SDK_INT >= 26 && enabled
|
|
|
|
cbNotificationMention.isEnabled = enabled
|
|
cbNotificationBoost.isEnabled = enabled
|
|
cbNotificationFavourite.isEnabled = enabled
|
|
cbNotificationFollow.isEnabled = enabled
|
|
cbNotificationFollowRequest.isEnabled = enabled
|
|
cbNotificationReaction.isEnabled = enabled
|
|
cbNotificationVote.isEnabled = enabled
|
|
cbNotificationPost.isEnabled = enabled
|
|
|
|
cbConfirmFollow.isEnabled = enabled
|
|
cbConfirmFollowLockedUser.isEnabled = enabled
|
|
cbConfirmUnfollow.isEnabled = enabled
|
|
cbConfirmBoost.isEnabled = enabled
|
|
cbConfirmFavourite.isEnabled = enabled
|
|
cbConfirmUnboost.isEnabled = enabled
|
|
cbConfirmUnfavourite.isEnabled = enabled
|
|
cbConfirmToot.isEnabled = enabled
|
|
|
|
val ti = TootInstance.getCached(a.apiHost.ascii)
|
|
if (ti == null) {
|
|
etMediaSizeMax.setText(a.image_max_megabytes ?: "")
|
|
etMovieSizeMax.setText(a.movie_max_megabytes ?: "")
|
|
} else {
|
|
etMediaSizeMax.setText(
|
|
a.image_max_megabytes
|
|
?: a.getImageMaxBytes(ti).div(1000000).toString()
|
|
)
|
|
etMovieSizeMax.setText(
|
|
a.movie_max_megabytes
|
|
?: a.getMovieMaxBytes(ti).div(1000000).toString()
|
|
)
|
|
}
|
|
|
|
val currentResizeConfig = a.getResizeConfig()
|
|
var index = imageResizeItems.indexOfFirst { it.config.spec == currentResizeConfig.spec }
|
|
log.d("ResizeItem current ${currentResizeConfig.spec} index=$index ")
|
|
if (index == -1) index =
|
|
imageResizeItems.indexOfFirst { it.config.spec == SavedAccount.defaultResizeConfig.spec }
|
|
spResizeImage.setSelection(index, false)
|
|
|
|
val currentPushPolicy = a.push_policy
|
|
index = pushPolicyItems.indexOfFirst { it.id == currentPushPolicy }
|
|
if (index == -1) index = 0
|
|
spPushPolicy.setSelection(index, false)
|
|
|
|
showVisibility()
|
|
showAcctColor()
|
|
}
|
|
|
|
private fun showAcctColor() {
|
|
val sa = this.account
|
|
val ac = AcctColor.load(sa)
|
|
tvUserCustom.backgroundColor = ac.color_bg
|
|
tvUserCustom.text = ac.nickname
|
|
tvUserCustom.textColor = ac.color_fg.notZero()
|
|
?: attrColor(R.attr.colorTimeSmall)
|
|
}
|
|
|
|
private fun saveUIToData() {
|
|
if (!::account.isInitialized) return
|
|
|
|
if (loading) return
|
|
|
|
account.visibility = visibility
|
|
account.dont_hide_nsfw = swNSFWOpen.isChecked
|
|
account.dont_show_timeout = swDontShowTimeout.isChecked
|
|
account.expand_cw = swExpandCW.isChecked
|
|
account.default_sensitive = swMarkSensitive.isChecked
|
|
account.notification_mention = cbNotificationMention.isChecked
|
|
account.notification_boost = cbNotificationBoost.isChecked
|
|
account.notification_favourite = cbNotificationFavourite.isChecked
|
|
account.notification_follow = cbNotificationFollow.isChecked
|
|
account.notification_follow_request = cbNotificationFollowRequest.isChecked
|
|
account.notification_reaction = cbNotificationReaction.isChecked
|
|
account.notification_vote = cbNotificationVote.isChecked
|
|
account.notification_post = cbNotificationPost.isChecked
|
|
|
|
account.sound_uri = notification_sound_uri ?: ""
|
|
|
|
account.confirm_follow = cbConfirmFollow.isChecked
|
|
account.confirm_follow_locked = cbConfirmFollowLockedUser.isChecked
|
|
account.confirm_unfollow = cbConfirmUnfollow.isChecked
|
|
account.confirm_boost = cbConfirmBoost.isChecked
|
|
account.confirm_favourite = cbConfirmFavourite.isChecked
|
|
account.confirm_unboost = cbConfirmUnboost.isChecked
|
|
account.confirm_unfavourite = cbConfirmUnfavourite.isChecked
|
|
account.confirm_post = cbConfirmToot.isChecked
|
|
account.default_text = etDefaultText.text.toString()
|
|
|
|
val num = etMaxTootChars.parseInt()
|
|
account.max_toot_chars = if (num != null && num >= 0) {
|
|
num
|
|
} else {
|
|
0
|
|
}
|
|
|
|
account.movie_max_megabytes = etMovieSizeMax.text.toString().trim()
|
|
account.image_max_megabytes = etMediaSizeMax.text.toString().trim()
|
|
account.image_resize = (
|
|
imageResizeItems.elementAtOrNull(spResizeImage.selectedItemPosition)?.config
|
|
?: SavedAccount.defaultResizeConfig
|
|
).spec
|
|
|
|
account.push_policy =
|
|
pushPolicyItems.elementAtOrNull(spPushPolicy.selectedItemPosition)?.id
|
|
|
|
account.saveSetting()
|
|
|
|
}
|
|
|
|
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
|
if (buttonView == cbLocked) {
|
|
if (!profile_busy) sendLocked(isChecked)
|
|
} else {
|
|
saveUIToData()
|
|
}
|
|
}
|
|
|
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
saveUIToData()
|
|
}
|
|
|
|
override fun onNothingSelected(parent: AdapterView<*>?) {
|
|
saveUIToData()
|
|
}
|
|
|
|
override fun onClick(v: View) {
|
|
when (v.id) {
|
|
R.id.btnAccessToken -> performAccessToken()
|
|
R.id.btnInputAccessToken -> inputAccessToken()
|
|
|
|
R.id.btnAccountRemove -> performAccountRemove()
|
|
R.id.btnLoadPreference -> performLoadPreference()
|
|
R.id.btnVisibility -> performVisibility()
|
|
R.id.btnOpenBrowser -> openBrowser("https://${account.apiHost.ascii}/")
|
|
R.id.btnPushSubscription -> startTest(force = true)
|
|
R.id.btnPushSubscriptionNotForce -> startTest(force = false)
|
|
R.id.btnResetNotificationTracking ->
|
|
PollingWorker.resetNotificationTracking(this, account)
|
|
|
|
R.id.btnUserCustom -> ActNickname.open(
|
|
this,
|
|
account.acct,
|
|
false,
|
|
REQUEST_CODE_ACCT_CUSTOMIZE
|
|
)
|
|
|
|
R.id.btnNotificationSoundEdit -> openNotificationSoundPicker()
|
|
|
|
R.id.btnNotificationSoundReset -> {
|
|
notification_sound_uri = ""
|
|
saveUIToData()
|
|
}
|
|
|
|
R.id.btnProfileAvatar -> pickAvatarImage()
|
|
|
|
R.id.btnProfileHeader -> pickHeaderImage()
|
|
|
|
R.id.btnDisplayName -> sendDisplayName()
|
|
|
|
R.id.btnNote -> sendNote()
|
|
|
|
R.id.btnFields -> sendFields()
|
|
|
|
R.id.btnNotificationStyleEdit ->
|
|
NotificationHelper.openNotificationChannelSetting(
|
|
this,
|
|
account,
|
|
NotificationHelper.TRACKING_NAME_DEFAULT
|
|
)
|
|
|
|
R.id.btnNotificationStyleEditReply ->
|
|
NotificationHelper.openNotificationChannelSetting(
|
|
this,
|
|
account,
|
|
NotificationHelper.TRACKING_NAME_REPLY
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun showVisibility() {
|
|
btnVisibility.text = Styler.getVisibilityString(this, account.isMisskey, visibility)
|
|
}
|
|
|
|
private fun performVisibility() {
|
|
|
|
val list = if (account.isMisskey) {
|
|
arrayOf(
|
|
// TootVisibility.WebSetting,
|
|
TootVisibility.Public,
|
|
TootVisibility.UnlistedHome,
|
|
TootVisibility.PrivateFollowers,
|
|
TootVisibility.LocalPublic,
|
|
TootVisibility.LocalHome,
|
|
TootVisibility.LocalFollowers,
|
|
TootVisibility.DirectSpecified,
|
|
TootVisibility.DirectPrivate
|
|
)
|
|
} else {
|
|
arrayOf(
|
|
TootVisibility.WebSetting,
|
|
TootVisibility.Public,
|
|
TootVisibility.UnlistedHome,
|
|
TootVisibility.PrivateFollowers,
|
|
TootVisibility.DirectSpecified
|
|
)
|
|
}
|
|
|
|
val caption_list = list.map {
|
|
Styler.getVisibilityCaption(this, account.isMisskey, it)
|
|
}.toTypedArray()
|
|
|
|
AlertDialog.Builder(this)
|
|
.setTitle(R.string.choose_visibility)
|
|
.setItems(caption_list) { _, which ->
|
|
if (which in list.indices) {
|
|
visibility = list[which]
|
|
showVisibility()
|
|
saveUIToData()
|
|
}
|
|
}
|
|
.setNegativeButton(R.string.cancel, null)
|
|
.show()
|
|
|
|
}
|
|
|
|
private fun performLoadPreference() {
|
|
|
|
TootTaskRunner(this).run(account, object : TootTask {
|
|
override suspend fun background(client: TootApiClient): TootApiResult? {
|
|
return client.request("/api/v1/preferences")
|
|
}
|
|
|
|
override suspend fun handleResult(result: TootApiResult?) {
|
|
result ?: return
|
|
|
|
val json = result.jsonObject
|
|
if (json == null) {
|
|
showToast(true, result.error)
|
|
return
|
|
}
|
|
|
|
var bChanged = false
|
|
try {
|
|
loading = true
|
|
|
|
val tmpVisibility =
|
|
TootVisibility.parseMastodon(json.string("posting:default:visibility"))
|
|
if (tmpVisibility != null) {
|
|
bChanged = true
|
|
visibility = tmpVisibility
|
|
showVisibility()
|
|
}
|
|
|
|
val tmpDefaultSensitive = json.boolean("posting:default:sensitive")
|
|
if (tmpDefaultSensitive != null) {
|
|
bChanged = true
|
|
swMarkSensitive.isChecked = tmpDefaultSensitive
|
|
}
|
|
|
|
val tmpExpandMedia = json.string("reading:expand:media")
|
|
if (tmpExpandMedia?.isNotEmpty() == true) {
|
|
bChanged = true
|
|
swNSFWOpen.isChecked = (tmpExpandMedia == "show_all")
|
|
}
|
|
|
|
val tmpExpandCW = json.boolean("reading:expand:spoilers")
|
|
if (tmpExpandCW != null) {
|
|
bChanged = true
|
|
swExpandCW.isChecked = tmpExpandCW
|
|
}
|
|
|
|
} finally {
|
|
loading = false
|
|
if (bChanged) saveUIToData()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
///////////////////////////////////////////////////
|
|
private fun performAccountRemove() {
|
|
AlertDialog.Builder(this)
|
|
.setTitle(R.string.confirm)
|
|
.setMessage(R.string.confirm_account_remove)
|
|
.setNegativeButton(R.string.cancel, null)
|
|
.setPositiveButton(R.string.ok) { _, _ ->
|
|
account.delete()
|
|
|
|
val pref = pref()
|
|
if (account.db_id == Pref.lpTabletTootDefaultAccount(pref)) {
|
|
pref.edit().put(Pref.lpTabletTootDefaultAccount, -1L).apply()
|
|
}
|
|
|
|
finish()
|
|
|
|
GlobalScope.launch(Dispatchers.IO) {
|
|
try {
|
|
val install_id = PrefDevice.prefDevice(this@ActAccountSetting)
|
|
.getString(PrefDevice.KEY_INSTALL_ID, null)
|
|
if (install_id?.isEmpty() != false)
|
|
error("missing install_id")
|
|
|
|
val tag = account.notification_tag
|
|
if (tag?.isEmpty() != false)
|
|
error("missing notification_tag")
|
|
|
|
val call = App1.ok_http_client.newCall(
|
|
("instance_url=" + "https://${account.apiHost.ascii}".encodePercent()
|
|
+ "&app_id=" + packageName.encodePercent()
|
|
+ "&tag=" + tag
|
|
)
|
|
.toFormRequestBody()
|
|
.toPost()
|
|
.url(PollingWorker.APP_SERVER + "/unregister")
|
|
.build()
|
|
)
|
|
|
|
val response = call.await()
|
|
|
|
log.e("performAccountRemove: %s", response)
|
|
} catch (ex: Throwable) {
|
|
log.trace(ex, "performAccountRemove failed.")
|
|
}
|
|
}
|
|
}
|
|
.show()
|
|
}
|
|
|
|
///////////////////////////////////////////////////
|
|
private fun performAccessToken() {
|
|
|
|
TootTaskRunner(this@ActAccountSetting).run(account, object : TootTask {
|
|
override suspend fun background(client: TootApiClient): TootApiResult? {
|
|
return client.authentication1(
|
|
Pref.spClientName(this@ActAccountSetting),
|
|
forceUpdateClient = true
|
|
)
|
|
}
|
|
|
|
override suspend fun handleResult(result: TootApiResult?) {
|
|
result ?: return // cancelled.
|
|
|
|
val uri = result.string.mayUri()
|
|
val error = result.error
|
|
when {
|
|
uri != null -> {
|
|
val data = Intent()
|
|
data.data = uri
|
|
setResult(Activity.RESULT_OK, data)
|
|
finish()
|
|
}
|
|
|
|
error != null -> {
|
|
showToast(true, error)
|
|
log.e("can't get oauth browser URL. $error")
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
private fun inputAccessToken() {
|
|
|
|
val data = Intent()
|
|
data.putExtra(EXTRA_DB_ID, account.db_id)
|
|
setResult(RESULT_INPUT_ACCESS_TOKEN, data)
|
|
finish()
|
|
}
|
|
|
|
private fun openNotificationSoundPicker() {
|
|
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
|
|
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION)
|
|
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, R.string.notification_sound)
|
|
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false)
|
|
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false)
|
|
|
|
notification_sound_uri.mayUri()?.let { uri ->
|
|
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, uri)
|
|
}
|
|
|
|
val chooser = Intent.createChooser(intent, getString(R.string.notification_sound))
|
|
startActivityForResult(chooser, REQUEST_CODE_NOTIFICATION_SOUND)
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
private fun initializeProfile() {
|
|
// 初期状態
|
|
val question_id = R.drawable.wide_question
|
|
ivProfileAvatar.setErrorImage(defaultColorIcon(this, question_id))
|
|
ivProfileAvatar.setDefaultImage(defaultColorIcon(this, question_id))
|
|
|
|
val loadingText = when (account.isPseudo) {
|
|
true -> "(disabled for pseudo account)"
|
|
else -> "(loading…)"
|
|
}
|
|
etDisplayName.setText(loadingText)
|
|
etNote.setText(loadingText)
|
|
|
|
// 初期状態では編集不可能
|
|
btnProfileAvatar.isEnabled = false
|
|
btnProfileHeader.isEnabled = false
|
|
etDisplayName.isEnabled = false
|
|
btnDisplayName.isEnabled = false
|
|
etNote.isEnabled = false
|
|
btnNote.isEnabled = false
|
|
cbLocked.isEnabled = false
|
|
|
|
for (et in listEtFieldName) {
|
|
et.setText(loadingText)
|
|
et.isEnabled = false
|
|
}
|
|
for (et in listEtFieldValue) {
|
|
et.setText(loadingText)
|
|
et.isEnabled = false
|
|
}
|
|
|
|
// 疑似アカウントなら編集不可のまま
|
|
if (!account.isPseudo) loadProfile()
|
|
}
|
|
|
|
private fun loadProfile() {
|
|
// サーバから情報をロードする
|
|
|
|
TootTaskRunner(this).run(account, object : TootTask {
|
|
|
|
var data: TootAccount? = null
|
|
override suspend fun background(client: TootApiClient): TootApiResult? {
|
|
if (account.isMisskey) {
|
|
val result = client.request(
|
|
"/api/i",
|
|
account.putMisskeyApiToken().toPostRequestBuilder()
|
|
)
|
|
val jsonObject = result?.jsonObject
|
|
if (jsonObject != null) {
|
|
data = TootParser(this@ActAccountSetting, account).account(jsonObject)
|
|
if (data == null) return TootApiResult("TootAccount parse failed.")
|
|
}
|
|
return result
|
|
|
|
} else {
|
|
|
|
var result = account.checkConfirmed(this@ActAccountSetting, client)
|
|
if (result == null || result.error != null) return result
|
|
|
|
result = client.request("/api/v1/accounts/verify_credentials")
|
|
val jsonObject = result?.jsonObject
|
|
if (jsonObject != null) {
|
|
data = TootParser(this@ActAccountSetting, account).account(jsonObject)
|
|
if (data == null) return TootApiResult("TootAccount parse failed.")
|
|
}
|
|
return result
|
|
|
|
}
|
|
}
|
|
|
|
override suspend fun handleResult(result: TootApiResult?) {
|
|
if (result == null) return // cancelled.
|
|
|
|
val data = this.data
|
|
if (data != null) {
|
|
showProfile(data)
|
|
} else {
|
|
showToast(true, result.error)
|
|
}
|
|
|
|
}
|
|
})
|
|
}
|
|
|
|
var profile_busy: Boolean = false
|
|
|
|
internal fun showProfile(src: TootAccount) {
|
|
|
|
if (isDestroyed) return
|
|
|
|
profile_busy = true
|
|
try {
|
|
ivProfileAvatar.setImageUrl(
|
|
App1.pref,
|
|
Styler.calcIconRound(ivProfileAvatar.layoutParams),
|
|
src.avatar_static,
|
|
src.avatar
|
|
)
|
|
|
|
ivProfileHeader.setImageUrl(
|
|
App1.pref,
|
|
0f,
|
|
src.header_static,
|
|
src.header
|
|
)
|
|
|
|
val decodeOptions = DecodeOptions(
|
|
context = this@ActAccountSetting,
|
|
linkHelper = account,
|
|
emojiMapProfile = src.profile_emojis,
|
|
emojiMapCustom = src.custom_emojis,
|
|
mentionDefaultHostDomain = account
|
|
)
|
|
|
|
val display_name = src.display_name
|
|
val name = decodeOptions.decodeEmoji(display_name)
|
|
etDisplayName.setText(name)
|
|
name_invalidator.register(name)
|
|
|
|
val noteString = src.source?.note ?: src.note
|
|
val noteSpannable = when {
|
|
account.isMisskey -> {
|
|
SpannableString(noteString ?: "")
|
|
}
|
|
|
|
else -> {
|
|
decodeOptions.decodeEmoji(noteString)
|
|
}
|
|
}
|
|
|
|
etNote.setText(noteSpannable)
|
|
note_invalidator.register(noteSpannable)
|
|
|
|
cbLocked.isChecked = src.locked
|
|
|
|
// 編集可能にする
|
|
btnProfileAvatar.isEnabled = true
|
|
btnProfileHeader.isEnabled = true
|
|
etDisplayName.isEnabled = true
|
|
btnDisplayName.isEnabled = true
|
|
etNote.isEnabled = true
|
|
btnNote.isEnabled = true
|
|
cbLocked.isEnabled = true
|
|
|
|
if (src.source?.fields != null) {
|
|
val fields = src.source.fields
|
|
listEtFieldName.forEachIndexed { i, et ->
|
|
val handler = et.handler // may null
|
|
if (handler != null) {
|
|
// いつからかfields name にもカスタム絵文字が使えるようになった
|
|
// https://github.com/tootsuite/mastodon/pull/11350
|
|
// しかし
|
|
val text = decodeOptions.decodeEmoji(
|
|
when {
|
|
i >= fields.size -> ""
|
|
else -> fields[i].name
|
|
}
|
|
)
|
|
et.setText(text)
|
|
et.isEnabled = true
|
|
val invalidator = NetworkEmojiInvalidator(handler, et)
|
|
invalidator.register(text)
|
|
}
|
|
}
|
|
|
|
listEtFieldValue.forEachIndexed { i, et ->
|
|
val handler = et.handler // may null
|
|
if (handler != null) {
|
|
val text = decodeOptions.decodeEmoji(
|
|
when {
|
|
i >= fields.size -> ""
|
|
else -> fields[i].value
|
|
}
|
|
)
|
|
et.setText(text)
|
|
et.isEnabled = true
|
|
val invalidator = NetworkEmojiInvalidator(handler, et)
|
|
invalidator.register(text)
|
|
}
|
|
}
|
|
|
|
} else {
|
|
val fields = src.fields
|
|
|
|
listEtFieldName.forEachIndexed { i, et ->
|
|
val handler = et.handler // may null
|
|
if (handler != null) {
|
|
// いつからかfields name にもカスタム絵文字が使えるようになった
|
|
// https://github.com/tootsuite/mastodon/pull/11350
|
|
val text = decodeOptions.decodeEmoji(
|
|
when {
|
|
fields == null || i >= fields.size -> ""
|
|
else -> fields[i].name
|
|
}
|
|
)
|
|
et.setText(text)
|
|
et.isEnabled = true
|
|
val invalidator = NetworkEmojiInvalidator(handler, et)
|
|
invalidator.register(text)
|
|
}
|
|
|
|
}
|
|
|
|
listEtFieldValue.forEachIndexed { i, et ->
|
|
val handler = et.handler // may null
|
|
if (handler != null) {
|
|
val text = decodeOptions.decodeHTML(
|
|
when {
|
|
fields == null || i >= fields.size -> ""
|
|
else -> fields[i].value
|
|
}
|
|
)
|
|
et.text = text
|
|
et.isEnabled = true
|
|
val invalidator = NetworkEmojiInvalidator(handler, et)
|
|
invalidator.register(text)
|
|
}
|
|
}
|
|
}
|
|
|
|
} finally {
|
|
profile_busy = false
|
|
}
|
|
}
|
|
|
|
private fun updateCredential(key: String, value: Any) {
|
|
updateCredential(listOf(Pair(key, value)))
|
|
}
|
|
|
|
private fun updateCredential(args: List<Pair<String, Any>>) {
|
|
|
|
TootTaskRunner(this).run(account, object : TootTask {
|
|
|
|
private suspend fun uploadImageMisskey(
|
|
client: TootApiClient,
|
|
opener: InputStreamOpener
|
|
): Pair<TootApiResult?, TootAttachment?> {
|
|
|
|
val size = getStreamSize(true, opener.open())
|
|
|
|
val multipart_builder = MultipartBody.Builder()
|
|
.setType(MultipartBody.FORM)
|
|
|
|
val apiKey =
|
|
account.token_info?.string(TootApiClient.KEY_API_KEY_MISSKEY)
|
|
if (apiKey?.isNotEmpty() == true) {
|
|
multipart_builder.addFormDataPart("i", apiKey)
|
|
}
|
|
|
|
multipart_builder.addFormDataPart(
|
|
"file",
|
|
getDocumentName(contentResolver, opener.uri),
|
|
object : RequestBody() {
|
|
override fun contentType(): MediaType {
|
|
return opener.mimeType.toMediaType()
|
|
}
|
|
|
|
override fun contentLength(): Long {
|
|
return size
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
var ta: TootAttachment? = null
|
|
val result = client.request(
|
|
"/api/drive/files/create",
|
|
multipart_builder.build().toPost()
|
|
)?.also { result ->
|
|
val jsonObject = result.jsonObject
|
|
if (jsonObject != null) {
|
|
ta = parseItem(::TootAttachment, ServiceType.MISSKEY, jsonObject)
|
|
if (ta == null) result.error = "TootAttachment.parse failed"
|
|
}
|
|
}
|
|
|
|
return Pair(result, ta)
|
|
}
|
|
|
|
var data: TootAccount? = null
|
|
override suspend fun background(client: TootApiClient): TootApiResult? {
|
|
|
|
try {
|
|
if (account.isMisskey) {
|
|
val params = account.putMisskeyApiToken()
|
|
|
|
for (arg in args) {
|
|
val key = arg.first
|
|
val value = arg.second
|
|
|
|
val misskeyKey = when (key) {
|
|
"header" -> "bannerId"
|
|
"avatar" -> "avatarId"
|
|
"display_name" -> "name"
|
|
"note" -> "description"
|
|
"locked" -> "isLocked"
|
|
else -> return TootApiResult("Misskey does not support property '${key}'")
|
|
}
|
|
|
|
when (value) {
|
|
is String -> params[misskeyKey] = value
|
|
is Boolean -> params[misskeyKey] = value
|
|
|
|
is InputStreamOpener -> {
|
|
val (result, ta) = uploadImageMisskey(client, value)
|
|
ta ?: return result
|
|
params[misskeyKey] = ta.id
|
|
}
|
|
}
|
|
}
|
|
|
|
val result =
|
|
client.request("/api/i/update", params.toPostRequestBuilder())
|
|
val jsonObject = result?.jsonObject
|
|
if (jsonObject != null) {
|
|
val a = TootParser(this@ActAccountSetting, account).account(jsonObject)
|
|
?: return TootApiResult("TootAccount parse failed.")
|
|
data = a
|
|
}
|
|
|
|
return result
|
|
|
|
} else {
|
|
val multipart_body_builder = MultipartBody.Builder()
|
|
.setType(MultipartBody.FORM)
|
|
|
|
for (arg in args) {
|
|
val key = arg.first
|
|
val value = arg.second
|
|
|
|
if (value is String) {
|
|
multipart_body_builder.addFormDataPart(key, value)
|
|
} else if (value is Boolean) {
|
|
multipart_body_builder.addFormDataPart(
|
|
key,
|
|
if (value) "true" else "false"
|
|
)
|
|
|
|
} else if (value is InputStreamOpener) {
|
|
|
|
val fileName = "%x".format(System.currentTimeMillis())
|
|
|
|
multipart_body_builder.addFormDataPart(
|
|
key,
|
|
fileName,
|
|
object : RequestBody() {
|
|
override fun contentType(): MediaType? {
|
|
return value.mimeType.toMediaType()
|
|
}
|
|
|
|
override fun writeTo(sink: BufferedSink) {
|
|
value.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)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
val result = client.request(
|
|
"/api/v1/accounts/update_credentials",
|
|
multipart_body_builder.build().toPatch()
|
|
)
|
|
val jsonObject = result?.jsonObject
|
|
if (jsonObject != null) {
|
|
val a = TootParser(this@ActAccountSetting, account).account(jsonObject)
|
|
?: return TootApiResult("TootAccount parse failed.")
|
|
data = a
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
} finally {
|
|
for (arg in args) {
|
|
val value = arg.second
|
|
(value as? InputStreamOpener)?.deleteTempFile()
|
|
}
|
|
}
|
|
}
|
|
|
|
override suspend fun handleResult(result: TootApiResult?) {
|
|
if (result == null) return // cancelled.
|
|
|
|
val data = this.data
|
|
if (data != null) {
|
|
showProfile(data)
|
|
} else {
|
|
showToast(true, result.error)
|
|
for (arg in args) {
|
|
val key = arg.first
|
|
val value = arg.second
|
|
if (key == "locked" && value is Boolean) {
|
|
profile_busy = true
|
|
cbLocked.isChecked = !value
|
|
profile_busy = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
private fun sendDisplayName(bConfirmed: Boolean = false) {
|
|
val sv = etDisplayName.text.toString()
|
|
if (!bConfirmed) {
|
|
val length = sv.codePointCount(0, sv.length)
|
|
if (length > max_length_display_name) {
|
|
AlertDialog.Builder(this)
|
|
.setMessage(
|
|
getString(
|
|
R.string.length_warning,
|
|
getString(R.string.display_name),
|
|
length,
|
|
max_length_display_name
|
|
)
|
|
)
|
|
.setNegativeButton(R.string.cancel, null)
|
|
.setPositiveButton(R.string.ok) { _, _ -> sendDisplayName(bConfirmed = true) }
|
|
.setCancelable(true)
|
|
.show()
|
|
return
|
|
}
|
|
}
|
|
updateCredential("display_name", EmojiDecoder.decodeShortCode(sv))
|
|
}
|
|
|
|
private fun sendNote(bConfirmed: Boolean = false) {
|
|
val sv = etNote.text.toString()
|
|
if (!bConfirmed) {
|
|
|
|
val length = TootAccount.countText(sv)
|
|
if (length > max_length_note) {
|
|
AlertDialog.Builder(this)
|
|
.setMessage(
|
|
getString(
|
|
R.string.length_warning,
|
|
getString(R.string.note),
|
|
length,
|
|
max_length_note
|
|
)
|
|
)
|
|
.setNegativeButton(R.string.cancel, null)
|
|
.setPositiveButton(R.string.ok) { _, _ -> sendNote(bConfirmed = true) }
|
|
.setCancelable(true)
|
|
.show()
|
|
return
|
|
}
|
|
}
|
|
updateCredential("note", EmojiDecoder.decodeShortCode(sv))
|
|
}
|
|
|
|
private fun sendLocked(willLocked: Boolean) {
|
|
updateCredential("locked", willLocked)
|
|
}
|
|
|
|
private fun sendFields(bConfirmed: Boolean = false) {
|
|
val args = ArrayList<Pair<String, String>>()
|
|
var lengthLongest = -1
|
|
for (i in listEtFieldName.indices) {
|
|
val k = listEtFieldName[i].text.toString().trim()
|
|
val v = listEtFieldValue[i].text.toString().trim()
|
|
args.add(Pair("fields_attributes[$i][name]", k))
|
|
args.add(Pair("fields_attributes[$i][value]", v))
|
|
|
|
lengthLongest = max(
|
|
lengthLongest,
|
|
max(
|
|
k.codePointCount(0, k.length),
|
|
v.codePointCount(0, v.length)
|
|
)
|
|
)
|
|
}
|
|
if (!bConfirmed && lengthLongest > max_length_fields) {
|
|
AlertDialog.Builder(this)
|
|
.setMessage(
|
|
getString(
|
|
R.string.length_warning,
|
|
getString(R.string.profile_metadata),
|
|
lengthLongest,
|
|
max_length_fields
|
|
)
|
|
)
|
|
.setNegativeButton(R.string.cancel, null)
|
|
.setPositiveButton(R.string.ok) { _, _ -> sendFields(bConfirmed = true) }
|
|
.setCancelable(true)
|
|
.show()
|
|
return
|
|
}
|
|
|
|
updateCredential(args)
|
|
}
|
|
|
|
private fun pickAvatarImage() {
|
|
openPicker(PERMISSION_REQUEST_AVATAR)
|
|
}
|
|
|
|
private fun pickHeaderImage() {
|
|
openPicker(PERMISSION_REQUEST_HEADER)
|
|
}
|
|
|
|
private fun openPicker(permission_request_code: Int) {
|
|
val permissionCheck = ContextCompat.checkSelfPermission(
|
|
this,
|
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
)
|
|
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
|
|
preparePermission(permission_request_code)
|
|
return
|
|
}
|
|
|
|
val a = ActionsDialog()
|
|
a.addAction(getString(R.string.pick_image)) {
|
|
performAttachment(
|
|
if (permission_request_code == PERMISSION_REQUEST_AVATAR)
|
|
REQUEST_CODE_AVATAR_ATTACHMENT
|
|
else
|
|
REQUEST_CODE_HEADER_ATTACHMENT
|
|
)
|
|
}
|
|
a.addAction(getString(R.string.image_capture)) {
|
|
performCamera(
|
|
if (permission_request_code == PERMISSION_REQUEST_AVATAR)
|
|
REQUEST_CODE_AVATAR_CAMERA
|
|
else
|
|
REQUEST_CODE_HEADER_CAMERA
|
|
)
|
|
}
|
|
a.show(this, null)
|
|
}
|
|
|
|
private fun preparePermission(request_code: Int) {
|
|
if (Build.VERSION.SDK_INT >= 23) {
|
|
// No explanation needed, we can request the permission.
|
|
|
|
ActivityCompat.requestPermissions(
|
|
this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), request_code
|
|
)
|
|
return
|
|
}
|
|
showToast(true, R.string.missing_permission_to_access_media)
|
|
}
|
|
|
|
override fun onRequestPermissionsResult(
|
|
requestCode: Int, permissions: Array<String>, grantResults: IntArray
|
|
) {
|
|
when (requestCode) {
|
|
PERMISSION_REQUEST_AVATAR, PERMISSION_REQUEST_HEADER ->
|
|
// If request is cancelled, the result arrays are empty.
|
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
openPicker(requestCode)
|
|
} else {
|
|
showToast(true, R.string.missing_permission_to_access_media)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun performAttachment(request_code: Int) {
|
|
try {
|
|
val intent = intentGetContent(false, getString(R.string.pick_image), arrayOf("image/*"))
|
|
startActivityForResult(intent, request_code)
|
|
} catch (ex: Throwable) {
|
|
log.trace(ex, "performAttachment failed.")
|
|
showToast(ex, "performAttachment failed.")
|
|
}
|
|
|
|
}
|
|
|
|
private fun performCamera(request_code: Int) {
|
|
|
|
try {
|
|
// カメラで撮影
|
|
val filename = System.currentTimeMillis().toString() + ".jpg"
|
|
val values = ContentValues()
|
|
values.put(MediaStore.Images.Media.TITLE, filename)
|
|
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
|
uriCameraImage =
|
|
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
|
|
|
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
|
intent.putExtra(MediaStore.EXTRA_OUTPUT, uriCameraImage)
|
|
|
|
startActivityForResult(intent, request_code)
|
|
} catch (ex: Throwable) {
|
|
log.trace(ex, "opening camera app failed.")
|
|
showToast(ex, "opening camera app failed.")
|
|
}
|
|
|
|
}
|
|
|
|
internal interface InputStreamOpener {
|
|
|
|
val mimeType: String
|
|
|
|
val uri: Uri
|
|
|
|
fun open(): InputStream
|
|
|
|
fun deleteTempFile()
|
|
}
|
|
|
|
private fun createOpener(uriArg: Uri, mime_type: String): InputStreamOpener {
|
|
|
|
while (true) {
|
|
try {
|
|
|
|
// 画像の種別
|
|
val is_jpeg = MIME_TYPE_JPEG == mime_type
|
|
val is_png = MIME_TYPE_PNG == mime_type
|
|
if (!is_jpeg && !is_png) {
|
|
log.d("createOpener: source is not jpeg or png")
|
|
break
|
|
}
|
|
|
|
// 設定からリサイズ指定を読む
|
|
val resize_to = 1280
|
|
|
|
val bitmap = createResizedBitmap(this, uriArg, resize_to)
|
|
if (bitmap != null) {
|
|
try {
|
|
val cache_dir = externalCacheDir
|
|
if (cache_dir == null) {
|
|
showToast(false, "getExternalCacheDir returns null.")
|
|
break
|
|
}
|
|
|
|
cache_dir.mkdir()
|
|
|
|
val temp_file = File(
|
|
cache_dir,
|
|
"tmp." + System.currentTimeMillis() + "." + Thread.currentThread().id
|
|
)
|
|
FileOutputStream(temp_file).use { os ->
|
|
if (is_jpeg) {
|
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, os)
|
|
} else {
|
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os)
|
|
}
|
|
}
|
|
|
|
return object : InputStreamOpener {
|
|
|
|
override val mimeType: String
|
|
get() = mime_type
|
|
|
|
override val uri: Uri
|
|
get() = uriArg
|
|
|
|
override fun open() = FileInputStream(temp_file)
|
|
|
|
override fun deleteTempFile() {
|
|
temp_file.delete()
|
|
}
|
|
}
|
|
} finally {
|
|
bitmap.recycle()
|
|
}
|
|
}
|
|
|
|
} catch (ex: Throwable) {
|
|
log.trace(ex, "Resizing image failed.")
|
|
showToast(ex, "Resizing image failed.")
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
return object : InputStreamOpener {
|
|
|
|
override val mimeType: String
|
|
get() = mime_type
|
|
|
|
override val uri: Uri
|
|
get() = uriArg
|
|
|
|
override fun open(): InputStream {
|
|
return contentResolver.openInputStream(uri) ?: error("openInputStream returns null")
|
|
}
|
|
|
|
override fun deleteTempFile() {
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun addAttachment(request_code: Int, uri: Uri, mime_type: String?) {
|
|
|
|
if (mime_type == null) {
|
|
showToast(false, "mime type is not provided.")
|
|
return
|
|
}
|
|
|
|
if (!mime_type.startsWith("image/")) {
|
|
showToast(false, "mime type is not image.")
|
|
return
|
|
}
|
|
|
|
runWithProgress(
|
|
"preparing image",
|
|
{ createOpener(uri, mime_type) },
|
|
{
|
|
updateCredential(
|
|
when (request_code) {
|
|
REQUEST_CODE_HEADER_ATTACHMENT, REQUEST_CODE_HEADER_CAMERA -> "header"
|
|
else -> "avatar"
|
|
},
|
|
it
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
private fun startTest(force: Boolean) {
|
|
val wps = PushSubscriptionHelper(applicationContext, account, verbose = true)
|
|
|
|
TootTaskRunner(this).run(account, object : TootTask {
|
|
|
|
override suspend fun background(client: TootApiClient): TootApiResult? {
|
|
return wps.updateSubscription(client, force = force)
|
|
}
|
|
|
|
override suspend fun handleResult(result: TootApiResult?) {
|
|
result ?: return
|
|
val log = wps.logString
|
|
if (log.isNotEmpty()) {
|
|
AlertDialog.Builder(this@ActAccountSetting)
|
|
.setMessage(log)
|
|
.setPositiveButton(R.string.close, null)
|
|
.show()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
|
|
}
|
|
|