Compare commits

...

4 Commits

32 changed files with 705 additions and 457 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.20" /> <option name="version" value="1.9.22" />
</component> </component>
</project> </project>

View File

@ -16,7 +16,9 @@
package org.jetbrains.anko package org.jetbrains.anko
import android.content.Context import android.content.Context
import android.os.Build
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.View import android.view.View
import androidx.annotation.DimenRes import androidx.annotation.DimenRes
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -32,25 +34,43 @@ const val XXXHDPI: Int = DisplayMetrics.DENSITY_XXXHIGH
const val MAXDPI: Int = 0xfffe const val MAXDPI: Int = 0xfffe
//returns dip(dp) dimension value in pixels // sp to px
fun DisplayMetrics.sp(sp: Float) =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, this)
fun Context.sp(sp: Float) = resources.displayMetrics.sp(sp)
fun Context.sp(sp: Int) = resources.displayMetrics.sp(sp.toFloat())
// px to sp
fun DisplayMetrics.px2sp(px: Float): Float = when {
Build.VERSION.SDK_INT >= 34 ->
TypedValue.deriveDimension(TypedValue.COMPLEX_UNIT_SP, px, this)
else -> try {
@Suppress("DEPRECATION")
px / scaledDensity
} catch (ex: Throwable) {
0f
}
}
fun Context.px2sp(px: Float): Float = resources.displayMetrics.px2sp(px)
fun Context.px2sp(px: Int): Float = resources.displayMetrics.px2sp(px.toFloat())
// dip to px
fun Context.dip(value: Int): Int = (value * resources.displayMetrics.density).toInt() fun Context.dip(value: Int): Int = (value * resources.displayMetrics.density).toInt()
fun Context.dip(value: Float): Int = (value * resources.displayMetrics.density).toInt() fun Context.dip(value: Float): Int = (value * resources.displayMetrics.density).toInt()
//return sp dimension value in pixels
fun Context.sp(value: Int): Int = (value * resources.displayMetrics.scaledDensity).toInt()
fun Context.sp(value: Float): Int = (value * resources.displayMetrics.scaledDensity).toInt()
//converts px value into dip or sp //converts px value into dip or sp
fun Context.px2dip(px: Int): Float = px.toFloat() / resources.displayMetrics.density fun Context.px2dip(px: Int): Float = px.toFloat() / resources.displayMetrics.density
fun Context.px2sp(px: Int): Float = px.toFloat() / resources.displayMetrics.scaledDensity
fun Context.dimen(@DimenRes resource: Int): Int = resources.getDimensionPixelSize(resource) fun Context.dimen(@DimenRes resource: Int): Int = resources.getDimensionPixelSize(resource)
//the same for nested DSL components //the same for nested DSL components
fun AnkoContext<*>.dip(value: Int): Int = ctx.dip(value) fun AnkoContext<*>.dip(value: Int): Int = ctx.dip(value)
fun AnkoContext<*>.dip(value: Float): Int = ctx.dip(value) fun AnkoContext<*>.dip(value: Float): Int = ctx.dip(value)
fun AnkoContext<*>.sp(value: Int): Int = ctx.sp(value) fun AnkoContext<*>.sp(value: Int) = ctx.sp(value)
fun AnkoContext<*>.sp(value: Float): Int = ctx.sp(value) fun AnkoContext<*>.sp(value: Float) = ctx.sp(value)
fun AnkoContext<*>.px2dip(px: Int): Float = ctx.px2dip(px) fun AnkoContext<*>.px2dip(px: Int): Float = ctx.px2dip(px)
fun AnkoContext<*>.px2sp(px: Int): Float = ctx.px2sp(px) fun AnkoContext<*>.px2sp(px: Int): Float = ctx.px2sp(px)
fun AnkoContext<*>.dimen(@DimenRes resource: Int): Int = ctx.dimen(resource) fun AnkoContext<*>.dimen(@DimenRes resource: Int): Int = ctx.dimen(resource)
@ -58,8 +78,8 @@ fun AnkoContext<*>.dimen(@DimenRes resource: Int): Int = ctx.dimen(resource)
//the same for the views //the same for the views
fun View.dip(value: Int): Int = context.dip(value) fun View.dip(value: Int): Int = context.dip(value)
fun View.dip(value: Float): Int = context.dip(value) fun View.dip(value: Float): Int = context.dip(value)
fun View.sp(value: Int): Int = context.sp(value) fun View.sp(value: Int) = context.sp(value)
fun View.sp(value: Float): Int = context.sp(value) fun View.sp(value: Float) = context.sp(value)
fun View.px2dip(px: Int): Float = context.px2dip(px) fun View.px2dip(px: Int): Float = context.px2dip(px)
fun View.px2sp(px: Int): Float = context.px2sp(px) fun View.px2sp(px: Int): Float = context.px2sp(px)
fun View.dimen(@DimenRes resource: Int): Int = context.dimen(resource) fun View.dimen(@DimenRes resource: Int): Int = context.dimen(resource)
@ -67,8 +87,8 @@ fun View.dimen(@DimenRes resource: Int): Int = context.dimen(resource)
//the same for Fragments //the same for Fragments
fun Fragment.dip(value: Int): Int = requireContext().dip(value) fun Fragment.dip(value: Int): Int = requireContext().dip(value)
fun Fragment.dip(value: Float): Int = requireContext().dip(value) fun Fragment.dip(value: Float): Int = requireContext().dip(value)
fun Fragment.sp(value: Int): Int = requireContext().sp(value) fun Fragment.sp(value: Int) = requireContext().sp(value)
fun Fragment.sp(value: Float): Int = requireContext().sp(value) fun Fragment.sp(value: Float) = requireContext().sp(value)
fun Fragment.px2dip(px: Int): Float = requireContext().px2dip(px) fun Fragment.px2dip(px: Int): Float = requireContext().px2dip(px)
fun Fragment.px2sp(px: Int): Float = requireContext().px2sp(px) fun Fragment.px2sp(px: Int): Float = requireContext().px2sp(px)
fun Fragment.dimen(@DimenRes resource: Int): Int = requireContext().dimen(resource) fun Fragment.dimen(@DimenRes resource: Int): Int = requireContext().dimen(resource)

View File

@ -45,7 +45,7 @@ android {
} }
kotlin { kotlin {
jvmToolchain( Vers.kotlinJvmToolchain) jvmToolchain(Vers.kotlinJvmToolchain)
} }
kotlinOptions { kotlinOptions {
jvmTarget = Vers.kotlinJvmTarget jvmTarget = Vers.kotlinJvmTarget
@ -65,5 +65,7 @@ dependencies {
api(project(":apng")) api(project(":apng"))
implementation(project(":base")) implementation(project(":base"))
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:${Vers.desugarLibVersion}")
testImplementation("junit:junit:${Vers.junitVersion}") testImplementation("junit:junit:${Vers.junitVersion}")
} }

View File

@ -6,7 +6,7 @@ import java.util.Properties
plugins { plugins {
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.serialization") kotlin("plugin.serialization")
id("com.google.devtools.ksp") id("com.google.devtools.ksp")
id("io.gitlab.arturbosch.detekt") id("io.gitlab.arturbosch.detekt")
} }
@ -150,7 +150,6 @@ android {
} }
} }
dependencies { dependencies {
// desugar_jdk_libs 2.0.0 は AGP 7.4.0-alpha10 以降を要求する // desugar_jdk_libs 2.0.0 は AGP 7.4.0-alpha10 以降を要求する
@ -172,7 +171,8 @@ dependencies {
implementation("com.github.UnifiedPush:android-connector:2.1.1") implementation("com.github.UnifiedPush:android-connector:2.1.1")
implementation("jp.wasabeef:glide-transformations:4.3.0") implementation("jp.wasabeef:glide-transformations:4.3.0")
implementation("com.github.androidmads:QRGenerator:1.0.1") // implementation("com.github.androidmads:QRGenerator:1.0.1")
implementation("com.github.alexzhirkevich:custom-qr-generator:1.6.2")
val apng4AndroidVersion = "2.25.0" val apng4AndroidVersion = "2.25.0"
implementation("com.github.penfeizhou.android.animation:apng:$apng4AndroidVersion") implementation("com.github.penfeizhou.android.animation:apng:$apng4AndroidVersion")

View File

@ -63,10 +63,14 @@
android:maxSdkVersion="32" android:maxSdkVersion="32"
tools:ignore="ScopedStorage" /> tools:ignore="ScopedStorage" />
<!-- Devices running Android 13 (API level 33) or higher -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- To handle the reselection within the app on Android 14 (API level 34) -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

View File

@ -14,43 +14,75 @@ import android.text.Editable
import android.text.SpannableString import android.text.SpannableString
import android.text.TextWatcher import android.text.TextWatcher
import android.view.View import android.view.View
import android.widget.* import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.ImageButton
import android.widget.Spinner
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.jrummyapps.android.colorpicker.ColorPickerDialog import com.jrummyapps.android.colorpicker.ColorPickerDialog
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.auth.AuthBase import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.auth.authRepo import jp.juggler.subwaytooter.api.auth.authRepo
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.ServiceType
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootAttachment
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.api.entity.parseItem
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.api.runApiTask2
import jp.juggler.subwaytooter.api.showApiError
import jp.juggler.subwaytooter.databinding.ActAccountSettingBinding import jp.juggler.subwaytooter.databinding.ActAccountSettingBinding
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.actionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.notification.* import jp.juggler.subwaytooter.notification.checkNotificationImmediate
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
import jp.juggler.subwaytooter.notification.resetNotificationTracking
import jp.juggler.subwaytooter.push.PushBase import jp.juggler.subwaytooter.push.PushBase
import jp.juggler.subwaytooter.push.pushRepo import jp.juggler.subwaytooter.push.pushRepo
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoAcctColor import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.util.*
import jp.juggler.util.* import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.AppDispatchers import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.coroutine.launchProgress import jp.juggler.util.coroutine.launchProgress
import jp.juggler.util.data.* import jp.juggler.util.data.UriSerializer
import jp.juggler.util.data.getDocumentName
import jp.juggler.util.data.getStreamSize
import jp.juggler.util.data.notZero
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption import jp.juggler.util.log.withCaption
import jp.juggler.util.long
import jp.juggler.util.media.ResizeConfig import jp.juggler.util.media.ResizeConfig
import jp.juggler.util.media.ResizeType import jp.juggler.util.media.ResizeType
import jp.juggler.util.media.createResizedBitmap import jp.juggler.util.media.createResizedBitmap
import jp.juggler.util.network.toPatch import jp.juggler.util.network.toPatch
import jp.juggler.util.network.toPost import jp.juggler.util.network.toPost
import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.ui.* import jp.juggler.util.ui.ActivityResultHandler
import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.isEnabledAlpha
import jp.juggler.util.ui.isOk
import jp.juggler.util.ui.scan
import jp.juggler.util.ui.vg
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import okhttp3.MediaType import okhttp3.MediaType
@ -164,61 +196,39 @@ class ActAccountSetting : AppCompatActivity(),
loadLanguageList() loadLanguageList()
} }
/////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
private var permissionCamera = permissionSpecCamera.requester {
openCamera()
}
private var pickImageLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
private val pickImageCallback = ActivityResultCallback<Uri?> {
handlePickImageResult(it)
}
private val arCameraImage = ActivityResultHandler(log) {
handleCameraResult(it)
}
private val arShowAcctColor = ActivityResultHandler(log) { r -> private val arShowAcctColor = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler if (r.isOk) showAcctColor()
showAcctColor()
} }
private val arAddAttachment = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data
?.handleGetContentResult(contentResolver)
?.firstOrNull()
?.let {
uploadImage(
state.propName,
it.uri,
it.uri.resolveMimeType(it.mimeType, this),
)
}
}
private val arCameraImage = ActivityResultHandler(log) { r ->
if (r.isNotOk) {
// 失敗したら DBからデータを削除
state.uriCameraImage?.let {
contentResolver.delete(it, null, null)
}
state.uriCameraImage = null
} else {
// 画像のURL
val uri = r.data?.data ?: state.uriCameraImage
if (uri != null) {
uploadImage(
state.propName,
uri,
uri.resolveMimeType(null, this),
)
}
}
}
private val prPickAvater = permissionSpecImagePicker.requester { openPicker(it) }
private val prPickHeader = permissionSpecImagePicker.requester { openPicker(it) }
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
backPressed { handleBackPressed() } backPressed { handleBackPressed() }
prPickAvater.register(this) pickImageLauncher = registerForActivityResult(
prPickHeader.register(this) ActivityResultContracts.PickVisualMedia(),
pickImageCallback,
)
permissionCamera.register(this)
arShowAcctColor.register(this) arShowAcctColor.register(this)
arAddAttachment.register(this)
arCameraImage.register(this) arCameraImage.register(this)
if (savedInstanceState != null) { if (savedInstanceState != null) {
@ -1335,64 +1345,89 @@ class ActAccountSetting : AppCompatActivity(),
} }
private fun pickAvatarImage() { private fun pickAvatarImage() {
openPicker(prPickAvater) openImagePickerOrCamera("avatar")
} }
private fun pickHeaderImage() { private fun pickHeaderImage() {
openPicker(prPickHeader) openImagePickerOrCamera("header")
} }
private fun openPicker(permissionRequester: PermissionRequester) { private fun openImagePickerOrCamera(propName: String) {
state.propName = propName
launchAndShowError { launchAndShowError {
if (!permissionRequester.checkOrLaunch()) return@launchAndShowError
val propName = when (permissionRequester) {
prPickHeader -> "header"
else -> "avatar"
}
actionsDialog { actionsDialog {
action(getString(R.string.pick_image)) { action(getString(R.string.pick_image)) {
performAttachment(propName) openPickImage()
} }
action(getString(R.string.image_capture)) { action(getString(R.string.image_capture)) {
performCamera(propName) openCamera()
} }
} }
} }
} }
private fun performAttachment(propName: String) { private fun openPickImage() {
try { (pickImageLauncher ?: error("pickImageLauncher not registered")).launch(
state.propName = propName PickVisualMediaRequest(
val intent = intentGetContent(false, getString(R.string.pick_image), arrayOf("image/*")) ActivityResultContracts.PickVisualMedia.ImageOnly,
arAddAttachment.launch(intent) )
} catch (ex: Throwable) { )
log.e(ex, "performAttachment failed.")
showToast(ex, "performAttachment failed.")
}
} }
private fun performCamera(propName: String) { private fun handlePickImageResult(uri: Uri?) {
uri ?: return
uploadImage(
state.propName,
uri,
uri.resolveMimeType(null, this),
)
}
try { private fun openCamera() {
if (!permissionCamera.checkOrLaunch()) return
launchAndShowError(errorCaption = "openCamera failed.") {
// カメラで撮影 // カメラで撮影
val filename = System.currentTimeMillis().toString() + ".jpg" val filename = System.currentTimeMillis().toString() + ".jpg"
val values = ContentValues() val values = ContentValues().apply {
values.put(MediaStore.Images.Media.TITLE, filename) put(MediaStore.Images.Media.TITLE, filename)
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
}
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
state.uriCameraImage = uri .also { state.uriCameraImage = it }
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri) putExtra(MediaStore.EXTRA_OUTPUT, uri)
}
state.propName = propName
arCameraImage.launch(intent) arCameraImage.launch(intent)
} catch (ex: Throwable) {
log.e(ex, "opening camera app failed.")
showToast(ex, "opening camera app failed.")
} }
} }
private fun handleCameraResult(r: ActivityResult) {
when {
r.isOk -> {
// 画像のURL
val uri = r.data?.data ?: state.uriCameraImage
if (uri != null) {
uploadImage(
state.propName,
uri,
uri.resolveMimeType(null, this),
)
}
}
else -> {
// 失敗したら DBからデータを削除
state.uriCameraImage?.let {
contentResolver.delete(it, null, null)
}
state.uriCameraImage = null
}
}
}
///////////////////////////////////////////////////
internal interface InputStreamOpener { internal interface InputStreamOpener {
val mimeType: String val mimeType: String
val uri: Uri val uri: Uri

View File

@ -22,7 +22,6 @@ import jp.juggler.subwaytooter.actpost.CompletionHelper
import jp.juggler.subwaytooter.actpost.FeaturedTagCache import jp.juggler.subwaytooter.actpost.FeaturedTagCache
import jp.juggler.subwaytooter.actpost.addAttachment import jp.juggler.subwaytooter.actpost.addAttachment
import jp.juggler.subwaytooter.actpost.applyMushroomText import jp.juggler.subwaytooter.actpost.applyMushroomText
import jp.juggler.subwaytooter.actpost.rearrangeAttachments
import jp.juggler.subwaytooter.actpost.onPickCustomThumbnailImpl import jp.juggler.subwaytooter.actpost.onPickCustomThumbnailImpl
import jp.juggler.subwaytooter.actpost.onPostAttachmentCompleteImpl import jp.juggler.subwaytooter.actpost.onPostAttachmentCompleteImpl
import jp.juggler.subwaytooter.actpost.openAttachment import jp.juggler.subwaytooter.actpost.openAttachment
@ -33,6 +32,7 @@ import jp.juggler.subwaytooter.actpost.performAttachmentClick
import jp.juggler.subwaytooter.actpost.performMore import jp.juggler.subwaytooter.actpost.performMore
import jp.juggler.subwaytooter.actpost.performPost import jp.juggler.subwaytooter.actpost.performPost
import jp.juggler.subwaytooter.actpost.performSchedule import jp.juggler.subwaytooter.actpost.performSchedule
import jp.juggler.subwaytooter.actpost.rearrangeAttachments
import jp.juggler.subwaytooter.actpost.removeReply import jp.juggler.subwaytooter.actpost.removeReply
import jp.juggler.subwaytooter.actpost.resetSchedule import jp.juggler.subwaytooter.actpost.resetSchedule
import jp.juggler.subwaytooter.actpost.restoreState import jp.juggler.subwaytooter.actpost.restoreState
@ -82,7 +82,7 @@ import java.util.concurrent.ConcurrentHashMap
class ActPost : AppCompatActivity(), class ActPost : AppCompatActivity(),
View.OnClickListener, View.OnClickListener,
PostAttachment.Callback, PostAttachment.Callback,
MyClickableSpanHandler, AttachmentPicker.Callback { MyClickableSpanHandler {
companion object { companion object {
private val log = LogCategory("ActPost") private val log = LogCategory("ActPost")
@ -215,7 +215,21 @@ class ActPost : AppCompatActivity(),
appState = App1.getAppState(this) appState = App1.getAppState(this)
handler = appState.handler handler = appState.handler
attachmentUploader = AttachmentUploader(this, handler) attachmentUploader = AttachmentUploader(this, handler)
attachmentPicker = AttachmentPicker(this, this) attachmentPicker = AttachmentPicker(this, object : AttachmentPicker.Callback {
override suspend fun onPickAttachment(uri: Uri, mimeType: String?) {
addAttachment(uri, mimeType)
}
override suspend fun onPickCustomThumbnail(
attachmentId: String?,
src: GetContentResultEntry,
) {
val pa = attachmentList.find { it.attachment?.id?.toString() == attachmentId }
?: error("missing attachment for attachmentId=$attachmentId")
onPickCustomThumbnailImpl(pa, src)
}
})
density = resources.displayMetrics.density density = resources.displayMetrics.density
arMushroom.register(this) arMushroom.register(this)
@ -315,6 +329,7 @@ class ActPost : AppCompatActivity(),
R.id.btnFeaturedTag -> completionHelper.openFeaturedTagList( R.id.btnFeaturedTag -> completionHelper.openFeaturedTagList(
featuredTagCache[account?.acct?.ascii ?: ""]?.list featuredTagCache[account?.acct?.ascii ?: ""]?.list
) )
R.id.btnAttachmentsRearrange -> rearrangeAttachments() R.id.btnAttachmentsRearrange -> rearrangeAttachments()
R.id.ibSchedule -> performSchedule() R.id.ibSchedule -> performSchedule()
R.id.ibScheduleReset -> resetSchedule() R.id.ibScheduleReset -> resetSchedule()
@ -337,10 +352,6 @@ class ActPost : AppCompatActivity(),
openBrowser(span.linkInfo.url) openBrowser(span.linkInfo.url)
} }
override fun onPickAttachment(uri: Uri, mimeType: String?) {
addAttachment(uri, mimeType)
}
override fun onPostAttachmentProgress() { override fun onPostAttachmentProgress() {
launchIO { launchIO {
try { try {
@ -355,15 +366,6 @@ class ActPost : AppCompatActivity(),
onPostAttachmentCompleteImpl(pa) onPostAttachmentCompleteImpl(pa)
} }
override fun resumeCustomThumbnailTarget(id: String?): PostAttachment? {
id ?: return null
return attachmentList.find { it.attachment?.id?.toString() == id }
}
override fun onPickCustomThumbnail(pa: PostAttachment, src: GetContentResultEntry) {
onPickCustomThumbnailImpl(pa, src)
}
fun initUI() { fun initUI() {
setContentView(views.root) setContentView(views.root)

View File

@ -269,7 +269,9 @@ fun ActPost.performAttachmentClick(idx: Int) {
TootAttachmentType.GIFV, TootAttachmentType.GIFV,
TootAttachmentType.Video, TootAttachmentType.Video,
-> action(getString(R.string.custom_thumbnail)) { -> action(getString(R.string.custom_thumbnail)) {
attachmentPicker.openCustomThumbnail(pa) attachmentPicker.openCustomThumbnail(
attachmentId = pa.attachment?.id?.toString()
)
} }
else -> Unit else -> Unit

View File

@ -1,95 +1,101 @@
package jp.juggler.subwaytooter.dialog package jp.juggler.subwaytooter.dialog
import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.view.View import android.graphics.drawable.ColorDrawable
import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity
import android.widget.TextView import com.github.alexzhirkevich.customqrgenerator.QrData
import androidmads.library.qrgenearator.QRGContents import com.github.alexzhirkevich.customqrgenerator.vector.QrCodeDrawable
import androidmads.library.qrgenearator.QRGEncoder import com.github.alexzhirkevich.customqrgenerator.vector.createQrVectorOptions
import jp.juggler.subwaytooter.ActMain import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorBallShape
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorColor
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorFrameShape
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorLogoPadding
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorLogoShape
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorPixelShape
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.util.coroutine.launchProgress import jp.juggler.subwaytooter.databinding.DlgQrCodeBinding
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.withProgress
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.os.resDrawable
import kotlinx.coroutines.withContext
@SuppressLint("StaticFieldLeak") private val log = LogCategory("DlgQRCode")
object DlgQRCode {
private val log = LogCategory("DlgQRCode") val UInt.int get() = toInt()
internal interface QrCodeCallback { fun AppCompatActivity.dialogQrCode(
fun onQrCode(bitmap: Bitmap?) message: CharSequence,
} url: String,
) = launchAndShowError("dialogQrCode failed.") {
private fun makeQrCode( val drawable = withProgress(
activity: ActMain, caption = getString(R.string.generating_qr_code),
size: Int,
url: String,
callback: QrCodeCallback,
) { ) {
activity.launchProgress( withContext(AppDispatchers.DEFAULT) {
"making QR code", QrCodeDrawable(data = QrData.Url(url), options = qrCodeOptions())
progressInitializer = { }
it.setMessageEx(activity.getString(R.string.generating_qr_code)) }
}, val dialog = Dialog(this@dialogQrCode)
doInBackground = {
try { val views = DlgQrCodeBinding.inflate(layoutInflater).apply {
QRGEncoder( btnCancel.setOnClickListener { dialog.cancel() }
/* data */ url, ivQrCode.setImageDrawable(drawable)
/* bundle */ null, tvMessage.text = message
QRGContents.Type.TEXT, tvUrl.text = "[ $url ]" // なぜか素のURLだと@以降が表示されない
/* dimension */ size,
).apply {
// 背景色
colorBlack = Color.WHITE
// 図柄の色
colorWhite = Color.BLACK
}.bitmap
} catch (ex: Throwable) {
log.e(ex, "QR generation failed.")
null
}
},
afterProc = {
if (it != null) callback.onQrCode(it)
},
)
} }
fun open(activity: ActMain, message: CharSequence, url: String) { dialog.apply {
setContentView(views.root)
val size = (0.5f + 240f * activity.density).toInt() setCancelable(true)
makeQrCode(activity, size, url, object : QrCodeCallback { setCanceledOnTouchOutside(true)
show()
@SuppressLint("InflateParams") }
override fun onQrCode(bitmap: Bitmap?) { }
val viewRoot = activity.layoutInflater.inflate(R.layout.dlg_qr_code, null, false) private fun AppCompatActivity.qrCodeOptions() = createQrVectorOptions {
val dialog = Dialog(activity) background {
dialog.setContentView(viewRoot) drawable = ColorDrawable(Color.WHITE)
dialog.setCancelable(true) }
dialog.setCanceledOnTouchOutside(true)
padding = .125f
var tv = viewRoot.findViewById<TextView>(R.id.tvMessage)
tv.text = message logo {
drawable = resDrawable(R.drawable.qr_code_center)
tv = viewRoot.findViewById(R.id.tvUrl) size = .25f
tv.text = "[ $url ]" // なぜか素のURLだと@以降が表示されない shape = QrVectorLogoShape.Default
padding = QrVectorLogoPadding.Natural(.1f)
val iv = viewRoot.findViewById<ImageView>(R.id.ivQrCode) }
iv.setImageBitmap(bitmap) shapes {
// 市松模様のドット
dialog.setOnDismissListener { darkPixel = QrVectorPixelShape.RoundCorners(.5f)
iv.setImageDrawable(null) // 3隅の真ん中の大きめドット
bitmap?.recycle() ball = QrVectorBallShape.RoundCorners(.25f)
} // 3隅の枠
frame = QrVectorFrameShape.RoundCorners(.25f)
viewRoot.findViewById<View>(R.id.btnCancel).setOnClickListener { dialog.cancel() } }
colors {
dialog.show() val cobalt = 0xFF0088FFU.int
} val cobaltDark = 0xFF004488U.int
}) // 市松模様のドット
dark = QrVectorColor.Solid(cobaltDark)
// 3隅の真ん中の大きめドット
ball = QrVectorColor.RadialGradient(
colors = listOf(
0f to cobaltDark,
1f to cobalt,
),
radius = 2f,
)
// 3隅の枠
frame = QrVectorColor.LinearGradient(
colors = listOf(
0f to cobaltDark,
1f to cobalt,
),
orientation = QrVectorColor.LinearGradient
.Orientation.Vertical
)
} }
} }

View File

@ -23,7 +23,7 @@ import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.databinding.DlgContextMenuBinding import jp.juggler.subwaytooter.databinding.DlgContextMenuBinding
import jp.juggler.subwaytooter.dialog.DlgListMember import jp.juggler.subwaytooter.dialog.DlgListMember
import jp.juggler.subwaytooter.dialog.DlgQRCode import jp.juggler.subwaytooter.dialog.dialogQrCode
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.span.MyClickableSpan import jp.juggler.subwaytooter.span.MyClickableSpan
@ -446,11 +446,11 @@ internal class DlgContextMenu(
else -> whoHost else -> whoHost
} }
private fun updateGroup(btn: Button, group: View, toggle: Boolean = false): Boolean { private fun updateGroup(btn: Button, group: View, toggle: Boolean = false) {
if (btn.visibility != View.VISIBLE) { if (btn.visibility != View.VISIBLE) {
group.vg(false) group.vg(false)
return true return
} }
when { when {
@ -463,51 +463,57 @@ internal class DlgContextMenu(
else -> btn.setOnClickListener(this) else -> btn.setOnClickListener(this)
} }
val iconId = if (group.visibility == View.VISIBLE) { val iconId = when (group.visibility) {
R.drawable.ic_arrow_drop_up View.VISIBLE -> R.drawable.ic_arrow_drop_up
} else { else -> R.drawable.ic_arrow_drop_down
R.drawable.ic_arrow_drop_down
} }
val iconColor = activity.attrColor(R.attr.colorTimeSmall) val iconColor = activity.attrColor(R.attr.colorTimeSmall)
val drawable = createColoredDrawable(activity, iconId, iconColor, 1f) val drawable = createColoredDrawable(activity, iconId, iconColor, 1f)
btn.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null) btn.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, null, null, null)
return true
} }
private fun onClickUpdateGroup(v: View): Boolean = when (v.id) { private fun onClickUpdateGroup(v: View): Boolean {
R.id.btnGroupStatusCrossAccount -> updateGroup( when (v.id) {
views.btnGroupStatusCrossAccount, R.id.btnGroupStatusCrossAccount -> updateGroup(
views.llGroupStatusCrossAccount, views.btnGroupStatusCrossAccount,
toggle = true views.llGroupStatusCrossAccount,
) toggle = true
)
R.id.btnGroupUserCrossAccount -> updateGroup( R.id.btnGroupUserCrossAccount -> updateGroup(
views.btnGroupUserCrossAccount, views.btnGroupUserCrossAccount,
views.llGroupUserCrossAccount, views.llGroupUserCrossAccount,
toggle = true toggle = true
) )
R.id.btnGroupStatusAround -> updateGroup(
views.btnGroupStatusAround, R.id.btnGroupStatusAround -> updateGroup(
views.llGroupStatusAround, views.btnGroupStatusAround,
toggle = true views.llGroupStatusAround,
) toggle = true
R.id.btnGroupStatusByMe -> updateGroup( )
views.btnGroupStatusByMe,
views.llGroupStatusByMe, R.id.btnGroupStatusByMe -> updateGroup(
toggle = true views.btnGroupStatusByMe,
) views.llGroupStatusByMe,
R.id.btnGroupStatusExtra -> updateGroup( toggle = true
views.btnGroupStatusExtra, )
views.llGroupStatusExtra,
toggle = true R.id.btnGroupStatusExtra -> updateGroup(
) views.btnGroupStatusExtra,
R.id.btnGroupUserExtra -> updateGroup( views.llGroupStatusExtra,
views.btnGroupUserExtra, toggle = true
views.llGroupUserExtra, )
toggle = true
) R.id.btnGroupUserExtra -> updateGroup(
else -> false views.btnGroupUserExtra,
views.llGroupUserExtra,
toggle = true
)
else -> return false
}
return true
} }
private fun ActMain.onClickUserAndStatus( private fun ActMain.onClickUserAndStatus(
@ -552,16 +558,18 @@ internal class DlgContextMenu(
accessInfo, accessInfo,
who who
) )
R.id.btnNickname -> clickNicknameCustomize(accessInfo, who) R.id.btnNickname -> clickNicknameCustomize(accessInfo, who)
R.id.btnAccountQrCode -> DlgQRCode.open( R.id.btnAccountQrCode -> activity.dialogQrCode(
activity, message = whoRef.decoded_display_name,
whoRef.decoded_display_name, url = who.getUserUrl()
who.getUserUrl()
) )
R.id.btnDomainBlock -> clickDomainBlock(accessInfo, who) R.id.btnDomainBlock -> clickDomainBlock(accessInfo, who)
R.id.btnOpenTimeline -> who.apiHost.valid()?.let { timelineLocal(pos, it) } R.id.btnOpenTimeline -> who.apiHost.valid()?.let { timelineLocal(pos, it) }
R.id.btnDomainTimeline -> who.apiHost.valid() R.id.btnDomainTimeline -> who.apiHost.valid()
?.let { timelineDomain(pos, accessInfo, it) } ?.let { timelineDomain(pos, accessInfo, it) }
R.id.btnAvatarImage -> openAvatarImage(who) R.id.btnAvatarImage -> openAvatarImage(who)
R.id.btnQuoteName -> quoteName(who) R.id.btnQuoteName -> quoteName(who)
R.id.btnHideBoost -> userSetShowBoosts(accessInfo, who, false) R.id.btnHideBoost -> userSetShowBoosts(accessInfo, who, false)
@ -574,6 +582,7 @@ internal class DlgContextMenu(
column, column,
getUserApiHost() getUserApiHost()
) )
R.id.btnEndorse -> userEndorsement(accessInfo, who, !relation.endorsed) R.id.btnEndorse -> userEndorsement(accessInfo, who, !relation.endorsed)
R.id.btnCopyAccountId -> who.id.toString().copyToClipboard(activity) R.id.btnCopyAccountId -> who.id.toString().copyToClipboard(activity)
R.id.btnOpenAccountInAdminWebUi -> openBrowser("https://${accessInfo.apiHost.ascii}/admin/accounts/${who.id}") R.id.btnOpenAccountInAdminWebUi -> openBrowser("https://${accessInfo.apiHost.ascii}/admin/accounts/${who.id}")
@ -612,6 +621,7 @@ internal class DlgContextMenu(
accessInfo, accessInfo,
status status
) )
R.id.btnQuoteUrlStatus -> openPost(status.url?.notEmpty()) R.id.btnQuoteUrlStatus -> openPost(status.url?.notEmpty())
R.id.btnShareUrlStatus -> shareText(status.url?.notEmpty()) R.id.btnShareUrlStatus -> shareText(status.url?.notEmpty())
R.id.btnConversationMute -> conversationMute(accessInfo, status) R.id.btnConversationMute -> conversationMute(accessInfo, status)
@ -622,6 +632,7 @@ internal class DlgContextMenu(
accessInfo, accessInfo,
status status
) )
else -> return false else -> return false
} }
return true return true
@ -675,10 +686,12 @@ internal class DlgContextMenu(
dialog.dismissSafe() dialog.dismissSafe()
followFromAnotherAccount(pos, accessInfo, who) followFromAnotherAccount(pos, accessInfo, who)
} }
R.id.btnProfile -> { R.id.btnProfile -> {
dialog.dismissSafe() dialog.dismissSafe()
userProfileFromAnotherAccount(pos, accessInfo, who) userProfileFromAnotherAccount(pos, accessInfo, who)
} }
R.id.btnSendMessage -> { R.id.btnSendMessage -> {
dialog.dismissSafe() dialog.dismissSafe()
mentionFromAnotherAccount(accessInfo, who) mentionFromAnotherAccount(accessInfo, who)

View File

@ -20,7 +20,7 @@ import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.errorEx import jp.juggler.util.log.errorEx
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption import jp.juggler.util.log.withCaption
import jp.juggler.util.media.MovideResizeMode import jp.juggler.util.media.MovieResizeMode
import jp.juggler.util.media.MovieResizeConfig import jp.juggler.util.media.MovieResizeConfig
import jp.juggler.util.media.ResizeConfig import jp.juggler.util.media.ResizeConfig
import jp.juggler.util.media.ResizeType import jp.juggler.util.media.ResizeType
@ -928,7 +928,7 @@ class SavedAccount(
fun getMovieResizeConfig() = fun getMovieResizeConfig() =
MovieResizeConfig( MovieResizeConfig(
mode = MovideResizeMode.fromInt(movieTranscodeMode), mode = MovieResizeMode.fromInt(movieTranscodeMode),
limitBitrate = movieTranscodeBitrate.toLongOrNull() limitBitrate = movieTranscodeBitrate.toLongOrNull()
?.takeIf { it >= 100_000L } ?: 2_000_000L, ?.takeIf { it >= 100_000L } ?: 2_000_000L,
limitFrameRate = movieTranscodeFramerate.toIntOrNull() limitFrameRate = movieTranscodeFramerate.toIntOrNull()

View File

@ -4,6 +4,11 @@ import android.content.ContentValues
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.dialog.actionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
@ -17,8 +22,8 @@ import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.ui.ActivityResultHandler import jp.juggler.util.ui.ActivityResultHandler
import jp.juggler.util.ui.isNotOk import jp.juggler.util.ui.isNotOk
import jp.juggler.util.ui.isOk
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
class AttachmentPicker( class AttachmentPicker(
@ -33,9 +38,8 @@ class AttachmentPicker(
// callback after media selected // callback after media selected
interface Callback { interface Callback {
fun onPickAttachment(uri: Uri, mimeType: String? = null) suspend fun onPickAttachment(uri: Uri, mimeType: String? = null)
fun onPickCustomThumbnail(pa: PostAttachment, src: GetContentResultEntry) suspend fun onPickCustomThumbnail(attachmentId: String?, src: GetContentResultEntry)
fun resumeCustomThumbnailTarget(id: String?): PostAttachment?
} }
// actions after permission granted // actions after permission granted
@ -43,7 +47,6 @@ class AttachmentPicker(
@Serializable @Serializable
data class States( data class States(
@Serializable(with = UriSerializer::class) @Serializable(with = UriSerializer::class)
var uriCameraImage: Uri? = null, var uriCameraImage: Uri? = null,
@ -55,59 +58,48 @@ class AttachmentPicker(
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
// activity result handlers // activity result handlers
private val arAttachmentChooser = ActivityResultHandler(log) { r -> private var pickThumbnailLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
private val pickThumbnailCallback = ActivityResultCallback<Uri?> {
handleThumbnailResult(it)
}
private var pickVisualMediaLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
private val pickVisualMediaCallback = ActivityResultCallback<List<Uri>?> { uris ->
uris?.handleGetContentResult(activity.contentResolver)?.pickAll()
}
private val prPickAudio = permissionSpecAudioPicker.requester { openAudioPicker() }
private val arPickAudio = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler if (r.isNotOk) return@ActivityResultHandler
r.data?.handleGetContentResult(activity.contentResolver)?.pickAll() r.data?.handleGetContentResult(activity.contentResolver)?.pickAll()
} }
private val arCamera = ActivityResultHandler(log) { r -> private val prCamera = permissionSpecCamera.requester { openStillCamera() }
if (r.isNotOk) { private val arCamera = ActivityResultHandler(log) { handleCameraResult(it) }
// 失敗したら DBからデータを削除
states.uriCameraImage?.let { uri ->
activity.contentResolver.delete(uri, null, null)
states.uriCameraImage = null
}
} else {
// 画像のURL
when (val uri = r.data?.data ?: states.uriCameraImage) {
null -> activity.showToast(false, "missing image uri")
else -> callback.onPickAttachment(uri)
}
}
}
private val arCapture = ActivityResultHandler(log) { r -> private val prCapture = permissionSpecCapture.requester { openPicker() }
if (r.isNotOk) return@ActivityResultHandler private val arCapture = ActivityResultHandler(log) { handleCaptureResult(it) }
r.data?.data?.let { callback.onPickAttachment(it) }
}
private val arCustomThumbnail = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data
?.handleGetContentResult(activity.contentResolver)
?.firstOrNull()
?.let {
callback.resumeCustomThumbnailTarget(states.customThumbnailTargetId)?.let { pa ->
callback.onPickCustomThumbnail(pa, it)
}
}
}
private val prPickAttachment = permissionSpecImagePicker.requester { openPicker() }
private val prPickCustomThumbnail = permissionSpecImagePicker.requester {
callback.resumeCustomThumbnailTarget(states.customThumbnailTargetId)
?.let { openCustomThumbnail(it) }
}
init { init {
// must register all ARHs before onStart // must register all ARHs before onStart
prPickAttachment.register(activity) prPickAudio.register(activity)
prPickCustomThumbnail.register(activity) arPickAudio.register(activity)
arAttachmentChooser.register(activity)
prCamera.register(activity)
arCamera.register(activity) arCamera.register(activity)
arCapture.register(activity) arCapture.register(activity)
arCustomThumbnail.register(activity)
pickVisualMediaLauncher = activity.registerForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(4),
pickVisualMediaCallback,
)
pickThumbnailLauncher = activity.registerForActivityResult(
ActivityResultContracts.PickVisualMedia(),
pickThumbnailCallback,
)
} }
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
@ -132,21 +124,17 @@ class AttachmentPicker(
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
fun openPicker() { fun openPicker() {
if (!prPickAttachment.checkOrLaunch()) return
activity.run { activity.run {
launchAndShowError { launchAndShowError {
actionsDialog { actionsDialog {
action(getString(R.string.pick_images)) { action(getString(R.string.pick_images_or_video)) {
openAttachmentChooser(R.string.pick_images, "image/*", "video/*") openVisualMediaPicker()
}
action(getString(R.string.pick_videos)) {
openAttachmentChooser(R.string.pick_videos, "video/*")
} }
action(getString(R.string.pick_audios)) { action(getString(R.string.pick_audios)) {
openAttachmentChooser(R.string.pick_audios, "audio/*") openAudioPicker()
} }
action(getString(R.string.image_capture)) { action(getString(R.string.image_capture)) {
performCamera() openStillCamera()
} }
action(getString(R.string.video_capture)) { action(getString(R.string.video_capture)) {
performCapture( performCapture(
@ -165,43 +153,74 @@ class AttachmentPicker(
} }
} }
private fun openAttachmentChooser(titleId: Int, vararg mimeTypes: String) { private fun openVisualMediaPicker() {
// SAFのIntentで開く (pickVisualMediaLauncher
try { ?: error("pickVisualMediaLauncher is not registered."))
val intent = intentGetContent(true, activity.getString(titleId), mimeTypes) .launch(
arAttachmentChooser.launch(intent) PickVisualMediaRequest(
} catch (ex: Throwable) { ActivityResultContracts.PickVisualMedia.ImageAndVideo
log.e(ex, "openAttachmentChooser failed.") )
activity.showToast(ex, "openAttachmentChooser failed.") )
}
private fun openAudioPicker() {
if (!prPickAudio.checkOrLaunch()) return
activity.launchAndShowError {
val intent = intentGetContent(
allowMultiple = true,
caption = activity.getString(R.string.pick_audios),
mimeTypes = arrayOf("audio/*"),
)
arPickAudio.launch(intent)
} }
} }
private fun performCamera() { private fun openStillCamera() {
try { if (!prCamera.checkOrLaunch()) return
val values = ContentValues().apply { activity.launchAndShowError {
put(MediaStore.Images.Media.TITLE, "${System.currentTimeMillis()}.jpg") val newUri = activity.contentResolver.insert(
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") /* url = */
} MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
/* values = */
val newUri = ContentValues().apply {
activity.contentResolver.insert( put(MediaStore.Images.Media.TITLE, "${System.currentTimeMillis()}.jpg")
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
values },
) ).also { states.uriCameraImage = it }
.also { states.uriCameraImage = it }
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
putExtra(MediaStore.EXTRA_OUTPUT, newUri) putExtra(MediaStore.EXTRA_OUTPUT, newUri)
} }
arCamera.launch(intent) arCamera.launch(intent)
} catch (ex: Throwable) {
log.e(ex, "performCamera failed.")
activity.showToast(ex, "performCamera failed.")
} }
} }
private fun performCapture(action: String, errorCaption: String) { private fun handleCameraResult(r: ActivityResult) {
activity.launchAndShowError {
when {
r.isOk -> when (val uri = r.data?.data ?: states.uriCameraImage) {
null -> activity.showToast(false, "missing image uri")
else -> callback.onPickAttachment(uri)
}
// 失敗したら DBからデータを削除
else -> states.uriCameraImage?.let { uri ->
activity.contentResolver.delete(uri, null, null)
states.uriCameraImage = null
}
}
}
}
/**
* 動画や音声をキャプチャする
* - Uriは呼び出し先に任せっきり
*/
private fun performCapture(
action: String,
errorCaption: String,
) {
if (!prCapture.checkOrLaunch()) return
try { try {
arCapture.launch(Intent(action)) arCapture.launch(Intent(action))
} catch (ex: Throwable) { } catch (ex: Throwable) {
@ -210,28 +229,46 @@ class AttachmentPicker(
} }
} }
private fun ArrayList<GetContentResultEntry>.pickAll() = private fun handleCaptureResult(r: ActivityResult) {
forEach { callback.onPickAttachment(it.uri, it.mimeType) } activity.launchAndShowError {
if (r.isOk) {
when (val uri = r.data?.data) {
null -> activity.showToast(false, "missing media uri")
else -> callback.onPickAttachment(uri)
}
}
}
}
private fun List<GetContentResultEntry>.pickAll() {
activity.launchAndShowError {
forEach { callback.onPickAttachment(it.uri, it.mimeType) }
}
}
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Mastodon's custom thumbnail // Mastodon's custom thumbnail
fun openCustomThumbnail(pa: PostAttachment) { fun openCustomThumbnail(attachmentId: String?) {
try { states.customThumbnailTargetId = attachmentId
states.customThumbnailTargetId = pa.attachment?.id?.toString() ?: error("attachmentId is null")
?: return activity.launchAndShowError {
if (!prPickCustomThumbnail.checkOrLaunch()) return (pickThumbnailLauncher
// SAFのIntentで開く ?: error("pickThumbnailLauncher is not registered."))
arCustomThumbnail.launch( .launch(
intentGetContent( PickVisualMediaRequest(
false, ActivityResultContracts.PickVisualMedia.ImageOnly,
activity.getString(R.string.pick_images), )
arrayOf("image/*")
) )
) }
} catch (ex: Throwable) { }
log.e(ex, "openCustomThumbnail failed.")
activity.showToast(ex, "openCustomThumbnail failed.") private fun handleThumbnailResult(uri: Uri?) {
uri ?: return
activity.launchAndShowError {
listOf(uri).handleGetContentResult(activity.contentResolver).firstOrNull()?.let {
callback.onPickCustomThumbnail(states.customThumbnailTargetId, it)
}
} }
} }
} }

View File

@ -43,50 +43,111 @@ class PermissionSpec(
} }
} }
val permissionSpecNotification = if (Build.VERSION.SDK_INT >= 33) { val permissionSpecNotification = when {
PermissionSpec( Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
permissions = listOf( permissions = listOf(
Manifest.permission.POST_NOTIFICATIONS, Manifest.permission.POST_NOTIFICATIONS,
), ),
deniedId = R.string.permission_denied_notifications, deniedId = R.string.permission_denied_notifications,
rationalId = R.string.permission_rational_notifications, rationalId = R.string.permission_rational_notifications,
) )
} else { else -> PermissionSpec(
PermissionSpec(
permissions = emptyList(), permissions = emptyList(),
deniedId = R.string.permission_denied_notifications, deniedId = R.string.permission_denied_notifications,
rationalId = R.string.permission_rational_notifications, rationalId = R.string.permission_rational_notifications,
) )
} }
val permissionSpecMediaDownload = if (Build.VERSION.SDK_INT >= 33) { val permissionSpecMediaDownload = when {
PermissionSpec( Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
permissions = listOf(Manifest.permission.POST_NOTIFICATIONS), permissions = listOf(Manifest.permission.POST_NOTIFICATIONS),
deniedId = R.string.permission_denied_download_manager, deniedId = R.string.permission_denied_download_manager,
rationalId = R.string.permission_rational_download_manager, rationalId = R.string.permission_rational_download_manager,
) )
} else { else -> PermissionSpec(
PermissionSpec(
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
deniedId = R.string.permission_denied_media_access, deniedId = R.string.permission_denied_media_access,
rationalId = R.string.permission_rational_media_access, rationalId = R.string.permission_rational_media_access,
) )
} }
val permissionSpecImagePicker = if (Build.VERSION.SDK_INT >= 33) { /**
PermissionSpec( * 静止画のみを端末から選ぶ
*/
val permissionSpecImagePicker = when {
Build.VERSION.SDK_INT >= 34 -> PermissionSpec(
permissions = emptyList(),
deniedId = R.string.permission_denied_media_access,
rationalId = R.string.permission_rational_media_access,
)
Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
permissions = listOf( permissions = listOf(
Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.READ_MEDIA_AUDIO,
), ),
deniedId = R.string.permission_denied_media_access, deniedId = R.string.permission_denied_media_access,
rationalId = R.string.permission_rational_media_access, rationalId = R.string.permission_rational_media_access,
) )
} else { else -> PermissionSpec(
PermissionSpec(
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
deniedId = R.string.permission_denied_media_access, deniedId = R.string.permission_denied_media_access,
rationalId = R.string.permission_rational_media_access, rationalId = R.string.permission_rational_media_access,
) )
} }
/**
* オーディオのみを端末から選ぶ
*/
val permissionSpecAudioPicker = when {
Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
permissions = listOf(
Manifest.permission.READ_MEDIA_AUDIO,
),
deniedId = R.string.permission_denied_media_access,
rationalId = R.string.permission_rational_media_access,
)
else -> {
PermissionSpec(
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
deniedId = R.string.permission_denied_media_access,
rationalId = R.string.permission_rational_media_access,
)
}
}
/**
* カメラ撮影用の実行時パーミッション
* https://developer.android.com/media/camera/camera-deprecated/photobasics?hl=ja#TaskPhotoView
*/
val permissionSpecCamera = when {
Build.VERSION.SDK_INT >= 29 -> PermissionSpec(
permissions = emptyList(),
deniedId = R.string.permission_denied_media_access,
rationalId = R.string.permission_rational_media_access,
)
else -> PermissionSpec(
permissions = listOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
),
deniedId = R.string.permission_denied_media_access,
rationalId = R.string.permission_rational_media_access,
)
}
/**
* 動画や音声のキャプチャ前
*/
val permissionSpecCapture = when {
Build.VERSION.SDK_INT >= 29 -> PermissionSpec(
permissions = emptyList(),
deniedId = R.string.permission_denied_media_access,
rationalId = R.string.permission_rational_media_access,
)
else -> PermissionSpec(
permissions = listOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
),
deniedId = R.string.permission_denied_media_access,
rationalId = R.string.permission_rational_media_access,
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="64dp"/>
<stroke android:color="#08f" android:width="10dp"/>
</shape>

View File

@ -30,8 +30,8 @@
tools:ignore="SmallSp"/> tools:ignore="SmallSp"/>
<ImageView <ImageView
android:layout_width="240dp" android:layout_width="280dp"
android:layout_height="240dp" android:layout_height="280dp"
android:layout_margin="10dp" android:layout_margin="10dp"
android:scaleType="centerInside" android:scaleType="centerInside"
android:id="@+id/ivQrCode" android:id="@+id/ivQrCode"

View File

@ -537,6 +537,7 @@
<string name="performance">性能</string> <string name="performance">性能</string>
<string name="pick_image">画像を選択</string> <string name="pick_image">画像を選択</string>
<string name="pick_images">画像を選択…</string> <string name="pick_images">画像を選択…</string>
<string name="pick_images_or_video">画像や動画を選択…</string>
<string name="pick_videos">動画を選択…</string> <string name="pick_videos">動画を選択…</string>
<string name="pick_audios">音声ファイルを選択…</string> <string name="pick_audios">音声ファイルを選択…</string>
<string name="please_add_account">アカウントがありません。事前にアカウントの追加を行ってください。</string> <string name="please_add_account">アカウントがありません。事前にアカウントの追加を行ってください。</string>

View File

@ -320,6 +320,7 @@
<string name="color_and_background">Color and Background…</string> <string name="color_and_background">Color and Background…</string>
<string name="column_background">Column background</string> <string name="column_background">Column background</string>
<string name="pick_image">Pick an image</string> <string name="pick_image">Pick an image</string>
<string name="pick_images_or_video">Pick images or video…</string>
<string name="pick_images">Pick image(s)…</string> <string name="pick_images">Pick image(s)…</string>
<string name="pick_videos">Pick video(s)…</string> <string name="pick_videos">Pick video(s)…</string>
<string name="pick_audios">Choose audio file(s)…</string> <string name="pick_audios">Choose audio file(s)…</string>

View File

@ -77,7 +77,7 @@ dependencies {
api("androidx.emoji2:emoji2-views-helper:${Vers.emoji2Version}") api("androidx.emoji2:emoji2-views-helper:${Vers.emoji2Version}")
api("androidx.emoji2:emoji2-views:${Vers.emoji2Version}") api("androidx.emoji2:emoji2-views:${Vers.emoji2Version}")
api("androidx.emoji2:emoji2:${Vers.emoji2Version}") api("androidx.emoji2:emoji2:${Vers.emoji2Version}")
api("androidx.exifinterface:exifinterface:1.3.6") api("androidx.exifinterface:exifinterface:1.3.7")
api("androidx.lifecycle:lifecycle-common-java8:${Vers.lifecycleVersion}") api("androidx.lifecycle:lifecycle-common-java8:${Vers.lifecycleVersion}")
api("androidx.lifecycle:lifecycle-livedata-ktx:${Vers.lifecycleVersion}") api("androidx.lifecycle:lifecycle-livedata-ktx:${Vers.lifecycleVersion}")
api("androidx.lifecycle:lifecycle-process:${Vers.lifecycleVersion}") api("androidx.lifecycle:lifecycle-process:${Vers.lifecycleVersion}")
@ -100,14 +100,12 @@ dependencies {
api("com.otaliastudios:transcoder:0.10.5") api("com.otaliastudios:transcoder:0.10.5")
api("com.squareup.okhttp3:okhttp-urlconnection:${Vers.okhttpVersion}") api("com.squareup.okhttp3:okhttp-urlconnection:${Vers.okhttpVersion}")
api("com.squareup.okhttp3:okhttp:${Vers.okhttpVersion}") api("com.squareup.okhttp3:okhttp:${Vers.okhttpVersion}")
// api( "io.github.inflationx:calligraphy3:3.1.1")
// api( "io.github.inflationx:viewpump:2.1.1")
api("org.bouncycastle:bcprov-jdk15on:1.70") api("org.bouncycastle:bcprov-jdk15on:1.70")
api("org.jetbrains.kotlin:kotlin-reflect:${Vers.kotlinVersion}") api("org.jetbrains.kotlin:kotlin-reflect:${Vers.kotlinVersion}")
api("org.jetbrains.kotlinx:kotlinx-coroutines-android:${Vers.kotlinxCoroutinesVersion}") api("org.jetbrains.kotlinx:kotlinx-coroutines-android:${Vers.kotlinxCoroutinesVersion}")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Vers.kotlinxCoroutinesVersion}") api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Vers.kotlinxCoroutinesVersion}")
api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") api("org.jetbrains.kotlinx:kotlinx-serialization-json:${Vers.kotlinxSerializationLibVersion}")
api("ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0") api("ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0")
//non-OSS dependency api "androidx.media3:media3-cast:$media3Version" //non-OSS dependency api "androidx.media3:media3-cast:$media3Version"

View File

@ -96,11 +96,7 @@ suspend fun transcodeAudio(
} }
val transformer = Transformer.Builder(context) val transformer = Transformer.Builder(context)
.setLooper(looper) .setLooper(looper)
.setTransformationRequest( .setAudioMimeType(encodeMimeType)
TransformationRequest.Builder()
.setAudioMimeType(encodeMimeType)
.build()
)
.addListener(transformerListener) .addListener(transformerListener)
.build() .build()

View File

@ -153,23 +153,28 @@ fun AppCompatActivity.overrideActivityTransitionCompat(
@AnimRes animEnter: Int, @AnimRes animEnter: Int,
@AnimRes animExit: Int, @AnimRes animExit: Int,
) { ) {
if (Build.VERSION.SDK_INT >= 34) { when {
overrideActivityTransition( Build.VERSION.SDK_INT >= 34 -> {
when (overrideType) { overrideActivityTransition(
TransitionOverrideType.Open -> when (overrideType) {
AppCompatActivity.OVERRIDE_TRANSITION_OPEN TransitionOverrideType.Open ->
AppCompatActivity.OVERRIDE_TRANSITION_OPEN
TransitionOverrideType.Close -> TransitionOverrideType.Close ->
AppCompatActivity.OVERRIDE_TRANSITION_CLOSE AppCompatActivity.OVERRIDE_TRANSITION_CLOSE
}, },
animEnter, animEnter,
animExit animExit
) )
} else { }
overridePendingTransition(
animEnter, else -> {
animExit, @Suppress("DEPRECATION")
) overridePendingTransition(
animEnter,
animExit,
)
}
} }
} }

View File

@ -84,6 +84,30 @@ fun AppCompatActivity.launchAndShowError(
///////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////
suspend fun <T:Any?> AppCompatActivity.withProgress(
caption:String,
progressInitializer: suspend (ProgressDialogEx) -> Unit = {},
block: suspend (progress :ProgressDialogEx)->T,
):T {
val activity = this
var progress: ProgressDialogEx? = null
try {
progress = ProgressDialogEx(activity)
progress.setCancelable(true)
progress.isIndeterminateEx = true
progress.setMessageEx(caption)
progressInitializer(progress)
progress.show()
return supervisorScope {
val task = async(AppDispatchers.MainImmediate) { block(progress) }
progress.setOnCancelListener { task.cancel() }
task.await()
}
} finally {
progress?.dismissSafe()
}
}
fun <T : Any?> AppCompatActivity.launchProgress( fun <T : Any?> AppCompatActivity.launchProgress(
caption: String, caption: String,
doInBackground: suspend CoroutineScope.(ProgressDialogEx) -> T, doInBackground: suspend CoroutineScope.(ProgressDialogEx) -> T,

View File

@ -1,5 +1,6 @@
package jp.juggler.util.data package jp.juggler.util.data
import android.content.ClipData
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -283,28 +284,26 @@ data class GetContentResultEntry(
var time: Long? = null, var time: Long? = null,
) )
// returns list of pair of uri and mime-type. fun MutableList<GetContentResultEntry>.addNoDuplicate(
fun Intent.handleGetContentResult(contentResolver: ContentResolver): ArrayList<GetContentResultEntry> { contentResolver: ContentResolver,
val urlList = ArrayList<GetContentResultEntry>() uri: Uri?,
// 単一選択 type: String? = null,
data?.let { ) {
val mimeType = try { uri ?: return
type ?: contentResolver.getType(it) if (any { it.uri == uri }) return
} catch (ex: Throwable) { val mimeType = try {
log.w(ex, "contentResolver.getType failed. uri=$it") type ?: contentResolver.getType(uri)
null } catch (ex: Throwable) {
} log.w(ex, "contentResolver.getType failed. uri=$uri")
urlList.add(GetContentResultEntry(it, mimeType)) return
} }
// 複数選択 add(GetContentResultEntry(uri, mimeType))
this.clipData?.let { clipData -> }
for (uri in (0 until clipData.itemCount).mapNotNull { clipData.getItemAt(it)?.uri }) {
if (urlList.none { it.uri == uri }) { fun List<GetContentResultEntry>.grantPermissions(
urlList.add(GetContentResultEntry(uri)) contentResolver: ContentResolver,
} ) {
} forEach {
}
urlList.forEach {
try { try {
contentResolver.takePersistableUriPermission( contentResolver.takePersistableUriPermission(
it.uri, it.uri,
@ -313,5 +312,29 @@ fun Intent.handleGetContentResult(contentResolver: ContentResolver): ArrayList<G
} catch (_: Throwable) { } catch (_: Throwable) {
} }
} }
return urlList
} }
// returns list of pair of uri and mime-type.
fun List<Uri>.handleGetContentResult(contentResolver: ContentResolver) =
buildList {
this@handleGetContentResult.forEach {
addNoDuplicate(contentResolver, it)
}
grantPermissions(contentResolver)
}
val ClipData.uris
get() = (0 until itemCount).mapNotNull { getItemAt(it)?.uri }
// returns list of pair of uri and mime-type.
fun Intent.handleGetContentResult(contentResolver: ContentResolver) =
buildList {
// 単一選択
addNoDuplicate(contentResolver, data, type)
// 複数選択
clipData?.uris?.forEach {
addNoDuplicate(contentResolver, it)
}
grantPermissions(contentResolver)
}

View File

@ -289,18 +289,14 @@ fun createResizedBitmap(
// 出力用Bitmap作成 // 出力用Bitmap作成
val dst = Bitmap.createBitmap(dstSizeInt.x, dstSizeInt.y, Bitmap.Config.ARGB_8888) val dst = Bitmap.createBitmap(dstSizeInt.x, dstSizeInt.y, Bitmap.Config.ARGB_8888)
try { try {
if (dst == null) { val canvas = Canvas(dst)
context.showToast(false, "bitmap creation failed.") val paint = Paint()
} else { paint.isFilterBitmap = true
val canvas = Canvas(dst) canvas.drawBitmap(sourceBitmap, matrix, paint)
val paint = Paint() log.d("createResizedBitmap: resized to ${dstSizeInt.x}x${dstSizeInt.y}")
paint.isFilterBitmap = true return dst
canvas.drawBitmap(sourceBitmap, matrix, paint)
log.d("createResizedBitmap: resized to ${dstSizeInt.x}x${dstSizeInt.y}")
return dst
}
} catch (ex: Throwable) { } catch (ex: Throwable) {
dst?.recycle() dst.recycle()
throw ex throw ex
} }
} finally { } finally {

View File

@ -12,7 +12,6 @@ import androidx.media3.transformer.Effects
import androidx.media3.transformer.ExportException import androidx.media3.transformer.ExportException
import androidx.media3.transformer.ExportResult import androidx.media3.transformer.ExportResult
import androidx.media3.transformer.ProgressHolder import androidx.media3.transformer.ProgressHolder
import androidx.media3.transformer.TransformationRequest
import androidx.media3.transformer.Transformer import androidx.media3.transformer.Transformer
import androidx.media3.transformer.VideoEncoderSettings import androidx.media3.transformer.VideoEncoderSettings
import com.otaliastudios.transcoder.Transcoder import com.otaliastudios.transcoder.Transcoder
@ -23,9 +22,14 @@ import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
import jp.juggler.util.coroutine.AppDispatchers import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.data.clip import jp.juggler.util.data.clip
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import kotlinx.coroutines.* import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -38,19 +42,19 @@ import kotlin.math.sqrt
private val log = LogCategory("MovieUtils") private val log = LogCategory("MovieUtils")
enum class MovideResizeMode(val int: Int) { enum class MovieResizeMode(val int: Int) {
Auto(0), Auto(0),
No(1), No(1),
Always(2), Always(2),
; ;
companion object { companion object {
fun fromInt(i: Int) = values().find { it.int == i } ?: Auto fun fromInt(i: Int) = entries.find { it.int == i } ?: Auto
} }
} }
data class MovieResizeConfig( data class MovieResizeConfig(
val mode: MovideResizeMode, val mode: MovieResizeMode,
val limitFrameRate: Int, val limitFrameRate: Int,
val limitBitrate: Long, val limitBitrate: Long,
val limitSquarePixels: Int, val limitSquarePixels: Int,
@ -78,9 +82,9 @@ data class MovieResizeConfig(
// トランスコードをスキップする判定 // トランスコードをスキップする判定
fun isTranscodeRequired(info: VideoInfo) = when (mode) { fun isTranscodeRequired(info: VideoInfo) = when (mode) {
MovideResizeMode.No -> false MovieResizeMode.No -> false
MovideResizeMode.Always -> true MovieResizeMode.Always -> true
MovideResizeMode.Auto -> MovieResizeMode.Auto ->
info.squarePixels > limitSquarePixels || info.squarePixels > limitSquarePixels ||
(info.actualBps ?: 0).toFloat() > limitBitrate.toFloat() * 1.5f || (info.actualBps ?: 0).toFloat() > limitBitrate.toFloat() * 1.5f ||
(info.frameRatio == null || info.frameRatio < 1f || info.frameRatio > limitFrameRate) (info.frameRatio == null || info.frameRatio < 1f || info.frameRatio > limitFrameRate)
@ -129,9 +133,9 @@ suspend fun transcodeVideoMedia3Transformer(
withContext(AppDispatchers.MainImmediate) { withContext(AppDispatchers.MainImmediate) {
when (resizeConfig.mode) { when (resizeConfig.mode) {
MovideResizeMode.No -> return@withContext inFile MovieResizeMode.No -> return@withContext inFile
MovideResizeMode.Always -> Unit MovieResizeMode.Always -> Unit
MovideResizeMode.Auto -> { MovieResizeMode.Auto -> {
if (!resizeConfig.isTranscodeRequired(info)) { if (!resizeConfig.isTranscodeRequired(info)) {
log.i("transcodeVideoMedia3Transformer: transcode not required.") log.i("transcodeVideoMedia3Transformer: transcode not required.")
return@withContext inFile return@withContext inFile
@ -189,12 +193,6 @@ suspend fun transcodeVideoMedia3Transformer(
} }
}.build() }.build()
val request = TransformationRequest.Builder().apply {
setVideoMimeType(MimeTypes.VIDEO_H264)
setAudioMimeType(MimeTypes.AUDIO_AAC)
// ビットレートがないな…
}.build()
// 完了検知 // 完了検知
val completed = AtomicBoolean(false) val completed = AtomicBoolean(false)
val error = AtomicReference<Throwable>(null) val error = AtomicReference<Throwable>(null)
@ -226,9 +224,11 @@ suspend fun transcodeVideoMedia3Transformer(
// 開始 // 開始
val transformer = Transformer.Builder(context).apply { val transformer = Transformer.Builder(context).apply {
setEncoderFactory(encoderFactory) setEncoderFactory(encoderFactory)
setTransformationRequest(request) setAudioMimeType(MimeTypes.AUDIO_AAC)
setVideoMimeType(MimeTypes.VIDEO_H264)
addListener(listener) addListener(listener)
}.build() }.build()
transformer.start(editedMediaItem, outFile.canonicalPath) transformer.start(editedMediaItem, outFile.canonicalPath)
// 完了まで待機しつつ、定期的に進捗コールバックを呼ぶ // 完了まで待機しつつ、定期的に進捗コールバックを呼ぶ
@ -265,9 +265,9 @@ suspend fun transcodeVideo(
} }
when (resizeConfig.mode) { when (resizeConfig.mode) {
MovideResizeMode.No -> return@withContext inFile MovieResizeMode.No -> return@withContext inFile
MovideResizeMode.Always -> Unit MovieResizeMode.Always -> Unit
MovideResizeMode.Auto -> { MovieResizeMode.Auto -> {
if (info.squarePixels <= resizeConfig.limitSquarePixels && if (info.squarePixels <= resizeConfig.limitSquarePixels &&
(info.actualBps ?: 0).toFloat() <= resizeConfig.limitBitrate * 1.5f && (info.actualBps ?: 0).toFloat() <= resizeConfig.limitBitrate * 1.5f &&
(info.frameRatio?.toInt() ?: 0) <= resizeConfig.limitFrameRate (info.frameRatio?.toInt() ?: 0) <= resizeConfig.limitFrameRate

View File

@ -1,7 +1,9 @@
package jp.juggler.util.os package jp.juggler.util.os
import android.content.Context import android.content.Context
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
/** /**
* インストゥルメントテストのContextは * インストゥルメントテストのContextは
@ -18,3 +20,6 @@ val Context.applicationContextSafe: Context
fun Context.error(@StringRes resId: Int, vararg args: Any?): Nothing = fun Context.error(@StringRes resId: Int, vararg args: Any?): Nothing =
error(getString(resId, *args)) error(getString(resId, *args))
fun Context.resDrawable(@DrawableRes resId: Int) =
ContextCompat.getDrawable(this, resId)

View File

@ -378,6 +378,9 @@ val AppCompatActivity.isLiveActivity: Boolean
val ActivityResult.isNotOk val ActivityResult.isNotOk
get() = Activity.RESULT_OK != resultCode get() = Activity.RESULT_OK != resultCode
val ActivityResult.isOk
get() = Activity.RESULT_OK == resultCode
/** /**
* Ringtone pickerの処理結果のUriまたはnull * Ringtone pickerの処理結果のUriまたはnull
*/ */

View File

@ -21,7 +21,8 @@ buildscript {
} }
plugins { plugins {
id("org.jetbrains.kotlin.jvm") version (Vers.kotlinVersion) apply false kotlin("jvm") version (Vers.kotlinVersion) apply false
kotlin("plugin.serialization") version (Vers.kotlinxSerializationPluginVersion) apply true // !!
id("org.jetbrains.kotlin.android") version (Vers.kotlinVersion) apply false id("org.jetbrains.kotlin.android") version (Vers.kotlinVersion) apply false
id("com.google.devtools.ksp") version (Vers.kspVersion) apply false id("com.google.devtools.ksp") version (Vers.kspVersion) apply false
} }
@ -31,7 +32,7 @@ allprojects {
google() google()
mavenCentral() mavenCentral()
// com.github.androidmads:QRGenerator // alexzhirkevich/custom-qr-generator
maven(url = "https://jitpack.io") maven(url = "https://jitpack.io")
} }
} }

View File

@ -1,44 +1,51 @@
import org.gradle.api.JavaVersion import org.gradle.api.JavaVersion
@Suppress("MemberVisibilityCanBePrivate")
object Vers { object Vers {
const val stBuildToolsVersion = "34.0.0"
const val stCompileSdkVersion = 34
const val stTargetSdkVersion = 34
const val stMinSdkVersion = 26
val javaSourceCompatibility = JavaVersion.VERSION_1_8 val javaSourceCompatibility = JavaVersion.VERSION_1_8
val javaTargetCompatibility = JavaVersion.VERSION_1_8 val javaTargetCompatibility = JavaVersion.VERSION_1_8
const val androidGradlePruginVersion = "8.1.4" const val kotlinVersion = "1.9.22"
const val kotlinJvmTarget = "1.8"
const val kotlinJvmToolchain = 17
const val androidGradlePruginVersion = "8.2.1"
const val androidxAnnotationVersion = "1.6.0" const val androidxAnnotationVersion = "1.6.0"
const val androidxTestEspressoCoreVersion = "3.5.1" const val androidxTestEspressoCoreVersion = "3.5.1"
const val androidxTestExtJunitVersion = "1.1.5" const val androidxTestExtJunitVersion = "1.1.5"
const val androidxTestVersion = "1.5.0" const val androidxTestVersion = "1.5.0"
const val ankoVersion = "0.10.8"
// const val ankoVersion = "0.10.8"
const val appcompatVersion = "1.6.1" const val appcompatVersion = "1.6.1"
const val archVersion = "2.2.0" const val archVersion = "2.2.0"
const val commonsCodecVersion = "1.16.0" const val commonsCodecVersion = "1.16.0"
const val composeVersion = "1.0.5" const val composeVersion = "1.0.5"
const val conscryptVersion = "2.5.2" const val conscryptVersion = "2.5.2"
const val coreKtxVersion = "1.12.0" const val coreKtxVersion = "1.12.0"
const val desugarLibVersion = "2.0.3" const val desugarLibVersion = "2.0.4"
const val detektVersion = "1.23.4" const val detektVersion = "1.23.4"
const val emoji2Version = "1.4.0" const val emoji2Version = "1.4.0"
const val glideVersion = "4.15.1" const val glideVersion = "4.15.1"
const val junitVersion = "4.13.2" const val junitVersion = "4.13.2"
const val koinVersion = "3.5.0" const val koinVersion = "3.5.0"
const val kotlinJvmTarget = "1.8" const val kotlinTestVersion = kotlinVersion // "1.9.22"
const val kotlinJvmToolchain = 17
const val kotlinTestVersion = "1.9.20"
const val kotlinVersion = "1.9.20"
const val kotlinxCoroutinesVersion = "1.7.3" const val kotlinxCoroutinesVersion = "1.7.3"
const val kspVersion = "1.9.20-1.0.14" const val kotlinxSerializationPluginVersion = kotlinVersion
const val kotlinxSerializationLibVersion = "1.6.2"
const val kspVersion = "$kotlinVersion-1.0.16"
const val lifecycleVersion = "2.6.2" const val lifecycleVersion = "2.6.2"
const val materialVersion = "1.10.0" const val materialVersion = "1.11.0"
const val media3Version = "1.2.0" const val media3Version = "1.2.0"
const val okhttpVersion = "5.0.0-alpha.11" const val okhttpVersion = "5.0.0-alpha.11"
const val preferenceKtxVersion = "1.2.1" const val preferenceKtxVersion = "1.2.1"
const val stBuildToolsVersion = "34.0.0"
const val stCompileSdkVersion = 34
const val stMinSdkVersion = 26
const val stTargetSdkVersion = 34
const val startupVersion = "1.1.1" const val startupVersion = "1.1.1"
const val testKtxVersion = "1.5.0" const val testKtxVersion = "1.5.0"
const val webpDecoderVersion = "2.6.$glideVersion" const val webpDecoderVersion = "2.6.$glideVersion"
const val workVersion = "2.8.1" const val workVersion = "2.9.0"
} }

View File

@ -27,19 +27,19 @@ android {
} }
compileOptions { compileOptions {
sourceCompatibility =Vers. javaSourceCompatibility sourceCompatibility = Vers.javaSourceCompatibility
targetCompatibility =Vers.javaTargetCompatibility targetCompatibility = Vers.javaTargetCompatibility
} }
kotlin { kotlin {
jvmToolchain(Vers.kotlinJvmToolchain) jvmToolchain(Vers.kotlinJvmToolchain)
} }
kotlinOptions { kotlinOptions {
jvmTarget =Vers. kotlinJvmTarget jvmTarget = Vers.kotlinJvmTarget
} }
tasks.withType<KotlinCompile>().configureEach { tasks.withType<KotlinCompile>().configureEach {
kotlinOptions { kotlinOptions {
jvmTarget =Vers. kotlinJvmTarget jvmTarget = Vers.kotlinJvmTarget
} }
} }
} }

View File

@ -1,6 +1,6 @@
#Mon Jun 13 20:53:58 JST 2022 #Mon Jun 13 20:53:58 JST 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -40,6 +40,7 @@ android {
compileOptions { compileOptions {
sourceCompatibility = Vers.javaSourceCompatibility sourceCompatibility = Vers.javaSourceCompatibility
targetCompatibility = Vers.javaTargetCompatibility targetCompatibility = Vers.javaTargetCompatibility
isCoreLibraryDesugaringEnabled = true
} }
kotlinOptions { kotlinOptions {