2018-01-04 19:52:25 +01:00
|
|
|
package jp.juggler.subwaytooter
|
|
|
|
|
|
|
|
import android.Manifest
|
|
|
|
import android.annotation.SuppressLint
|
|
|
|
import android.app.DownloadManager
|
|
|
|
import android.content.ClipData
|
|
|
|
import android.content.ClipDescription
|
|
|
|
import android.content.ClipboardManager
|
|
|
|
import android.content.Intent
|
|
|
|
import android.content.pm.PackageManager
|
|
|
|
import android.graphics.Bitmap
|
|
|
|
import android.graphics.BitmapFactory
|
|
|
|
import android.net.Uri
|
|
|
|
import android.os.Build
|
|
|
|
import android.os.Bundle
|
|
|
|
import android.os.Environment
|
|
|
|
import android.os.SystemClock
|
|
|
|
import android.support.v4.app.ActivityCompat
|
|
|
|
import android.support.v4.content.ContextCompat
|
|
|
|
import android.support.v7.app.AppCompatActivity
|
|
|
|
import android.view.View
|
|
|
|
import android.view.Window
|
|
|
|
import android.widget.TextView
|
2018-04-05 22:47:25 +02:00
|
|
|
import com.google.android.exoplayer2.*
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
|
|
|
|
import com.google.android.exoplayer2.source.ExtractorMediaSource
|
2018-04-05 22:47:25 +02:00
|
|
|
import com.google.android.exoplayer2.source.MediaSourceEventListener
|
2018-01-04 19:52:25 +01:00
|
|
|
import com.google.android.exoplayer2.source.TrackGroupArray
|
|
|
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
|
|
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
2018-04-05 22:47:25 +02:00
|
|
|
import com.google.android.exoplayer2.ui.PlayerView
|
|
|
|
import com.google.android.exoplayer2.upstream.DataSpec
|
2018-01-04 19:52:25 +01:00
|
|
|
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter
|
|
|
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
|
|
|
import com.google.android.exoplayer2.util.Util
|
|
|
|
|
|
|
|
import java.util.LinkedList
|
|
|
|
|
|
|
|
import jp.juggler.subwaytooter.api.TootApiClient
|
|
|
|
import jp.juggler.subwaytooter.api.TootApiResult
|
|
|
|
import jp.juggler.subwaytooter.api.TootTask
|
|
|
|
import jp.juggler.subwaytooter.api.TootTaskRunner
|
2018-01-10 16:47:35 +01:00
|
|
|
import jp.juggler.subwaytooter.api.entity.*
|
2018-01-04 19:52:25 +01:00
|
|
|
import jp.juggler.subwaytooter.dialog.ActionsDialog
|
2018-01-21 13:46:36 +01:00
|
|
|
import jp.juggler.subwaytooter.util.*
|
2018-01-04 19:52:25 +01:00
|
|
|
import jp.juggler.subwaytooter.view.PinchBitmapView
|
2018-04-05 22:47:25 +02:00
|
|
|
import java.io.IOException
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
|
|
|
internal val log = LogCategory("ActMediaViewer")
|
|
|
|
|
|
|
|
internal val download_history_list = LinkedList<DownloadHistory>()
|
|
|
|
internal const val DOWNLOAD_REPEAT_EXPIRE = 3000L
|
|
|
|
internal const val short_limit = 5000L
|
|
|
|
|
|
|
|
private const val PERMISSION_REQUEST_CODE = 1
|
|
|
|
|
|
|
|
internal const val EXTRA_IDX = "idx"
|
|
|
|
internal const val EXTRA_DATA = "data"
|
2018-08-18 12:58:14 +02:00
|
|
|
internal const val EXTRA_SERVICE_TYPE = "serviceType"
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
internal fun <T : TootAttachmentLike> encodeMediaList(list : ArrayList<T>?) =
|
|
|
|
list?.encodeJson()?.toString() ?: "[]"
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-08-18 12:58:14 +02:00
|
|
|
internal fun decodeMediaList(serviceType : ServiceType, src : String?) =
|
|
|
|
parseList(::TootAttachment, serviceType, src?.toJsonArray())
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-08-18 12:58:14 +02:00
|
|
|
fun open(
|
|
|
|
activity : ActMain,
|
|
|
|
serviceType : ServiceType,
|
|
|
|
list : ArrayList<TootAttachmentLike>,
|
|
|
|
idx : Int
|
|
|
|
) {
|
2018-01-04 19:52:25 +01:00
|
|
|
val intent = Intent(activity, ActMediaViewer::class.java)
|
|
|
|
intent.putExtra(EXTRA_IDX, idx)
|
2018-08-18 12:58:14 +02:00
|
|
|
intent.putExtra(EXTRA_SERVICE_TYPE, serviceType.ordinal)
|
2018-01-04 19:52:25 +01:00
|
|
|
intent.putExtra(EXTRA_DATA, encodeMediaList(list))
|
|
|
|
activity.startActivity(intent)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
internal var idx : Int = 0
|
2018-01-18 19:09:36 +01:00
|
|
|
private lateinit var media_list : ArrayList<TootAttachment>
|
2018-08-18 12:58:14 +02:00
|
|
|
private lateinit var serviceType : ServiceType
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
private lateinit var pbvImage : PinchBitmapView
|
|
|
|
private lateinit var btnPrevious : View
|
|
|
|
private lateinit var btnNext : View
|
|
|
|
private lateinit var tvError : TextView
|
|
|
|
private lateinit var exoPlayer : SimpleExoPlayer
|
2018-04-05 22:47:25 +02:00
|
|
|
private lateinit var exoView : PlayerView
|
2018-01-04 19:52:25 +01:00
|
|
|
private lateinit var svDescription : View
|
|
|
|
private lateinit var tvDescription : TextView
|
|
|
|
private lateinit var tvStatus : TextView
|
|
|
|
|
|
|
|
internal var buffering_last_shown : Long = 0
|
|
|
|
|
|
|
|
private val player_listener = object : Player.EventListener {
|
2018-08-18 12:58:14 +02:00
|
|
|
|
2018-04-05 22:47:25 +02:00
|
|
|
override fun onTimelineChanged(
|
|
|
|
timeline : Timeline?,
|
|
|
|
manifest : Any?,
|
|
|
|
reason : Int
|
|
|
|
) {
|
|
|
|
log.d("exoPlayer onTimelineChanged manifest=$manifest reason=$reason")
|
|
|
|
}
|
|
|
|
|
2018-08-18 12:58:14 +02:00
|
|
|
override fun onSeekProcessed() {
|
2018-04-05 22:47:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onShuffleModeEnabledChanged(shuffleModeEnabled : Boolean) {
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
override fun onTracksChanged(
|
|
|
|
trackGroups : TrackGroupArray?,
|
|
|
|
trackSelections : TrackSelectionArray?
|
|
|
|
) {
|
2018-01-04 19:52:25 +01:00
|
|
|
log.d("exoPlayer onTracksChanged")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onLoadingChanged(isLoading : Boolean) {
|
|
|
|
// かなり頻繁に呼ばれる
|
2018-01-28 20:03:04 +01:00
|
|
|
// warning.d( "exoPlayer onLoadingChanged %s" ,isLoading );
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onPlayerStateChanged(playWhenReady : Boolean, playbackState : Int) {
|
|
|
|
// かなり頻繁に呼ばれる
|
2018-01-28 20:03:04 +01:00
|
|
|
// warning.d( "exoPlayer onPlayerStateChanged %s %s", playWhenReady, playbackState );
|
2018-01-04 19:52:25 +01:00
|
|
|
if(playWhenReady && playbackState == Player.STATE_BUFFERING) {
|
|
|
|
val now = SystemClock.elapsedRealtime()
|
|
|
|
if(now - buffering_last_shown >= short_limit && exoPlayer.duration >= short_limit) {
|
|
|
|
buffering_last_shown = now
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(this@ActMediaViewer, false, R.string.video_buffering)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
/*
|
|
|
|
exoPlayer.getDuration() may returns negative value (TIME_UNSET ,same as Long.MIN_VALUE + 1).
|
|
|
|
*/
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onRepeatModeChanged(repeatMode : Int) {
|
|
|
|
log.d("exoPlayer onRepeatModeChanged %d", repeatMode)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onPlayerError(error : ExoPlaybackException) {
|
|
|
|
log.d("exoPlayer onPlayerError")
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(this@ActMediaViewer, error, "player error.")
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
2018-04-05 22:47:25 +02:00
|
|
|
override fun onPositionDiscontinuity(reason : Int) {
|
|
|
|
log.d("exoPlayer onPositionDiscontinuity reason=$reason")
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
override fun onPlaybackParametersChanged(playbackParameters : PlaybackParameters?) {
|
2018-01-04 19:52:25 +01:00
|
|
|
log.d("exoPlayer onPlaybackParametersChanged")
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onSaveInstanceState(outState : Bundle?) {
|
|
|
|
super.onSaveInstanceState(outState)
|
2018-01-21 13:46:36 +01:00
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
outState ?: return
|
2018-01-21 13:46:36 +01:00
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
outState.putInt(EXTRA_IDX, idx)
|
2018-08-18 12:58:14 +02:00
|
|
|
outState.putInt(EXTRA_SERVICE_TYPE, serviceType.ordinal)
|
2018-01-04 19:52:25 +01:00
|
|
|
outState.putString(EXTRA_DATA, encodeMediaList(media_list))
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onCreate(savedInstanceState : Bundle?) {
|
|
|
|
super.onCreate(savedInstanceState)
|
|
|
|
App1.setActivityTheme(this, true, true)
|
|
|
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
|
|
|
|
|
|
|
val intent = intent
|
|
|
|
|
|
|
|
this.idx = savedInstanceState?.getInt(EXTRA_IDX) ?: intent.getIntExtra(EXTRA_IDX, idx)
|
|
|
|
|
2018-08-18 12:58:14 +02:00
|
|
|
this.serviceType = ServiceType.values()[
|
|
|
|
savedInstanceState?.getInt(EXTRA_SERVICE_TYPE)
|
|
|
|
?: intent.getIntExtra(EXTRA_SERVICE_TYPE, 0)
|
|
|
|
]
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
this.media_list = decodeMediaList(
|
2018-08-18 12:58:14 +02:00
|
|
|
serviceType,
|
|
|
|
savedInstanceState?.getString(EXTRA_DATA)
|
|
|
|
?: intent.getStringExtra(EXTRA_DATA)
|
2018-01-04 19:52:25 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
if(idx < 0 || idx >= media_list.size) idx = 0
|
|
|
|
|
|
|
|
initUI()
|
|
|
|
|
|
|
|
load()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onDestroy() {
|
|
|
|
super.onDestroy()
|
|
|
|
pbvImage.setBitmap(null)
|
|
|
|
exoPlayer.release()
|
|
|
|
exoPlayer
|
|
|
|
}
|
|
|
|
|
|
|
|
internal fun initUI() {
|
|
|
|
setContentView(R.layout.act_media_viewer)
|
|
|
|
pbvImage = findViewById(R.id.pbvImage)
|
|
|
|
btnPrevious = findViewById(R.id.btnPrevious)
|
|
|
|
btnNext = findViewById(R.id.btnNext)
|
|
|
|
exoView = findViewById(R.id.exoView)
|
|
|
|
tvError = findViewById(R.id.tvError)
|
|
|
|
svDescription = findViewById(R.id.svDescription)
|
|
|
|
tvDescription = findViewById(R.id.tvDescription)
|
|
|
|
tvStatus = findViewById(R.id.tvStatus)
|
|
|
|
|
|
|
|
val enablePaging = media_list.size > 1
|
|
|
|
btnPrevious.isEnabled = enablePaging
|
|
|
|
btnNext.isEnabled = enablePaging
|
|
|
|
btnPrevious.alpha = if(enablePaging) 1f else 0.3f
|
|
|
|
btnNext.alpha = if(enablePaging) 1f else 0.3f
|
|
|
|
|
|
|
|
btnPrevious.setOnClickListener(this)
|
|
|
|
btnNext.setOnClickListener(this)
|
|
|
|
findViewById<View>(R.id.btnDownload).setOnClickListener(this)
|
|
|
|
findViewById<View>(R.id.btnMore).setOnClickListener(this)
|
|
|
|
|
|
|
|
pbvImage.setCallback(object : PinchBitmapView.Callback {
|
|
|
|
override fun onSwipe(delta : Int) {
|
|
|
|
if(isDestroyed) return
|
|
|
|
loadDelta(delta)
|
|
|
|
}
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
override fun onMove(
|
|
|
|
bitmap_w : Float,
|
|
|
|
bitmap_h : Float,
|
|
|
|
tx : Float,
|
|
|
|
ty : Float,
|
|
|
|
scale : Float
|
|
|
|
) {
|
2018-01-04 19:52:25 +01:00
|
|
|
App1.getAppState(this@ActMediaViewer).handler.post(Runnable {
|
|
|
|
if(isDestroyed) return@Runnable
|
|
|
|
if(tvStatus.visibility == View.VISIBLE) {
|
2018-01-21 13:46:36 +01:00
|
|
|
tvStatus.text = getString(
|
|
|
|
R.string.zooming_of,
|
|
|
|
bitmap_w.toInt(),
|
|
|
|
bitmap_h.toInt(),
|
|
|
|
scale
|
|
|
|
)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
exoPlayer = ExoPlayerFactory.newSimpleInstance(this, DefaultTrackSelector())
|
|
|
|
exoPlayer.addListener(player_listener)
|
|
|
|
|
|
|
|
exoView.player = exoPlayer
|
|
|
|
}
|
|
|
|
|
|
|
|
internal fun loadDelta(delta : Int) {
|
|
|
|
if(media_list.size < 2) return
|
|
|
|
val size = media_list.size
|
|
|
|
idx = (idx + size + delta) % size
|
|
|
|
load()
|
|
|
|
}
|
|
|
|
|
|
|
|
internal fun load() {
|
|
|
|
|
|
|
|
exoPlayer.stop()
|
|
|
|
pbvImage.visibility = View.GONE
|
|
|
|
exoView.visibility = View.GONE
|
|
|
|
tvError.visibility = View.GONE
|
|
|
|
svDescription.visibility = View.GONE
|
|
|
|
tvStatus.visibility = View.GONE
|
|
|
|
|
|
|
|
if(idx < 0 || idx >= media_list.size) {
|
|
|
|
showError(getString(R.string.media_attachment_empty))
|
|
|
|
return
|
|
|
|
}
|
2018-01-10 16:47:35 +01:00
|
|
|
val ta = media_list[idx]
|
2018-01-18 19:09:36 +01:00
|
|
|
val description = ta.description
|
2018-01-21 13:46:36 +01:00
|
|
|
if(description?.isNotEmpty() == true) {
|
2018-01-18 19:09:36 +01:00
|
|
|
svDescription.visibility = View.VISIBLE
|
|
|
|
tvDescription.text = description
|
|
|
|
}
|
|
|
|
|
|
|
|
if(TootAttachmentLike.TYPE_IMAGE == ta.type) {
|
|
|
|
loadBitmap(ta)
|
|
|
|
} else if(TootAttachmentLike.TYPE_VIDEO == ta.type || TootAttachmentLike.TYPE_GIFV == ta.type) {
|
|
|
|
loadVideo(ta)
|
|
|
|
} else {
|
|
|
|
// maybe TYPE_UNKNOWN
|
|
|
|
showError(getString(R.string.media_attachment_type_error, ta.type))
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun showError(message : String) {
|
|
|
|
exoView.visibility = View.GONE
|
|
|
|
pbvImage.visibility = View.GONE
|
|
|
|
tvError.visibility = View.VISIBLE
|
|
|
|
tvError.text = message
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
@SuppressLint("StaticFieldLeak")
|
|
|
|
private fun loadVideo(ta : TootAttachment) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
val url = ta.getLargeUrl(App1.pref)
|
|
|
|
if(url == null) {
|
|
|
|
showError("missing media attachment url.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
exoView.visibility = View.VISIBLE
|
|
|
|
|
|
|
|
val defaultBandwidthMeter = DefaultBandwidthMeter()
|
|
|
|
val extractorsFactory = DefaultExtractorsFactory()
|
|
|
|
|
|
|
|
val dataSourceFactory = DefaultDataSourceFactory(
|
|
|
|
this, Util.getUserAgent(this, getString(R.string.app_name)), defaultBandwidthMeter
|
|
|
|
)
|
|
|
|
|
2018-04-05 22:47:25 +02:00
|
|
|
val mediaSource = ExtractorMediaSource.Factory(dataSourceFactory)
|
|
|
|
.setExtractorsFactory(extractorsFactory)
|
|
|
|
.createMediaSource(
|
|
|
|
Uri.parse(url),
|
|
|
|
App1.getAppState(this).handler,
|
|
|
|
mediaSourceEventListener
|
|
|
|
)
|
2018-08-18 12:58:14 +02:00
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
exoPlayer.prepare(mediaSource)
|
|
|
|
exoPlayer.playWhenReady = true
|
|
|
|
if(TootAttachmentLike.TYPE_GIFV == ta.type) {
|
|
|
|
exoPlayer.repeatMode = Player.REPEAT_MODE_ALL
|
|
|
|
} else {
|
|
|
|
exoPlayer.repeatMode = Player.REPEAT_MODE_OFF
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-18 12:58:14 +02:00
|
|
|
val mediaSourceEventListener = object : MediaSourceEventListener {
|
2018-04-05 22:47:25 +02:00
|
|
|
override fun onLoadStarted(
|
|
|
|
dataSpec : DataSpec?,
|
|
|
|
dataType : Int,
|
|
|
|
trackType : Int,
|
|
|
|
trackFormat : Format?,
|
|
|
|
trackSelectionReason : Int,
|
|
|
|
trackSelectionData : Any?,
|
|
|
|
mediaStartTimeMs : Long,
|
|
|
|
mediaEndTimeMs : Long,
|
|
|
|
elapsedRealtimeMs : Long
|
|
|
|
) {
|
|
|
|
log.d("onLoadStarted")
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onDownstreamFormatChanged(
|
|
|
|
trackType : Int,
|
|
|
|
trackFormat : Format?,
|
|
|
|
trackSelectionReason : Int,
|
|
|
|
trackSelectionData : Any?,
|
|
|
|
mediaTimeMs : Long
|
|
|
|
) {
|
|
|
|
log.d("onDownstreamFormatChanged")
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onUpstreamDiscarded(
|
|
|
|
trackType : Int,
|
|
|
|
mediaStartTimeMs : Long,
|
|
|
|
mediaEndTimeMs : Long
|
|
|
|
) {
|
|
|
|
log.d("onUpstreamDiscarded")
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onLoadCompleted(
|
|
|
|
dataSpec : DataSpec?,
|
|
|
|
dataType : Int,
|
|
|
|
trackType : Int,
|
|
|
|
trackFormat : Format?,
|
|
|
|
trackSelectionReason : Int,
|
|
|
|
trackSelectionData : Any?,
|
|
|
|
mediaStartTimeMs : Long,
|
|
|
|
mediaEndTimeMs : Long,
|
|
|
|
elapsedRealtimeMs : Long,
|
|
|
|
loadDurationMs : Long,
|
|
|
|
bytesLoaded : Long
|
|
|
|
) {
|
|
|
|
log.d("onLoadCompleted")
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onLoadCanceled(
|
|
|
|
dataSpec : DataSpec?,
|
|
|
|
dataType : Int,
|
|
|
|
trackType : Int,
|
|
|
|
trackFormat : Format?,
|
|
|
|
trackSelectionReason : Int,
|
|
|
|
trackSelectionData : Any?,
|
|
|
|
mediaStartTimeMs : Long,
|
|
|
|
mediaEndTimeMs : Long,
|
|
|
|
elapsedRealtimeMs : Long,
|
|
|
|
loadDurationMs : Long,
|
|
|
|
bytesLoaded : Long
|
|
|
|
) {
|
|
|
|
log.d("onLoadCanceled")
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onLoadError(
|
|
|
|
dataSpec : DataSpec?,
|
|
|
|
dataType : Int,
|
|
|
|
trackType : Int,
|
|
|
|
trackFormat : Format?,
|
|
|
|
trackSelectionReason : Int,
|
|
|
|
trackSelectionData : Any?,
|
|
|
|
mediaStartTimeMs : Long,
|
|
|
|
mediaEndTimeMs : Long,
|
|
|
|
elapsedRealtimeMs : Long,
|
|
|
|
loadDurationMs : Long,
|
|
|
|
bytesLoaded : Long,
|
|
|
|
error : IOException?,
|
|
|
|
wasCanceled : Boolean
|
|
|
|
) {
|
2018-08-18 12:58:14 +02:00
|
|
|
if(error != null) {
|
2018-04-05 22:47:25 +02:00
|
|
|
showError(error.withCaption("load error."))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
@SuppressLint("StaticFieldLeak")
|
|
|
|
private fun loadBitmap(ta : TootAttachment) {
|
2018-01-14 22:47:42 +01:00
|
|
|
val urlList = ta.getLargeUrlList(App1.pref)
|
2018-01-21 13:46:36 +01:00
|
|
|
if(urlList.isEmpty()) {
|
2018-01-04 19:52:25 +01:00
|
|
|
showError("missing media attachment url.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
tvStatus.visibility = View.VISIBLE
|
|
|
|
tvStatus.text = null
|
|
|
|
|
|
|
|
pbvImage.visibility = View.VISIBLE
|
|
|
|
pbvImage.setBitmap(null)
|
|
|
|
|
|
|
|
TootTaskRunner(this, TootTaskRunner.PROGRESS_HORIZONTAL).run(object : TootTask {
|
|
|
|
|
|
|
|
private val options = BitmapFactory.Options()
|
|
|
|
|
|
|
|
internal var bitmap : Bitmap? = null
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
private fun decodeBitmap(data : ByteArray, pixel_max : Int) : Bitmap? {
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
options.inJustDecodeBounds = true
|
|
|
|
options.inScaled = false
|
|
|
|
options.outWidth = 0
|
|
|
|
options.outHeight = 0
|
2018-01-10 16:47:35 +01:00
|
|
|
BitmapFactory.decodeByteArray(data, 0, data.size, options)
|
2018-01-04 19:52:25 +01:00
|
|
|
var w = options.outWidth
|
|
|
|
var h = options.outHeight
|
|
|
|
if(w <= 0 || h <= 0) {
|
|
|
|
log.e("can't decode bounds.")
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
var bits = 0
|
|
|
|
while(w > pixel_max || h > pixel_max) {
|
|
|
|
++ bits
|
|
|
|
w = w shr 1
|
|
|
|
h = h shr 1
|
|
|
|
}
|
|
|
|
options.inJustDecodeBounds = false
|
|
|
|
options.inSampleSize = 1 shl bits
|
|
|
|
return BitmapFactory.decodeByteArray(data, 0, data.size, options)
|
|
|
|
}
|
|
|
|
|
2018-01-14 22:47:42 +01:00
|
|
|
internal fun getHttpCached(client : TootApiClient, url : String) : TootApiResult? {
|
|
|
|
val result = TootApiResult.makeWithCaption(url)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-04-30 16:01:00 +02:00
|
|
|
val request = okhttp3.Request.Builder()
|
|
|
|
.url(url)
|
|
|
|
.cacheControl(App1.CACHE_5MIN)
|
2018-08-18 12:58:14 +02:00
|
|
|
.addHeader("Accept", "image/webp,image/*,*/*;q=0.8")
|
2018-04-30 16:01:00 +02:00
|
|
|
.build()
|
|
|
|
|
2018-08-18 12:58:14 +02:00
|
|
|
if(! client.sendRequest(result, cached = true) {
|
2018-04-30 16:01:00 +02:00
|
|
|
request
|
2018-01-21 13:46:36 +01:00
|
|
|
}) return result
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
if(client.isApiCancelled) return null
|
2018-01-14 22:47:42 +01:00
|
|
|
val response = requireNotNull(result.response)
|
2018-01-04 19:52:25 +01:00
|
|
|
if(! response.isSuccessful) {
|
2018-01-21 13:46:36 +01:00
|
|
|
return result.setError(TootApiClient.formatResponse(response, result.caption))
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
2018-08-18 12:58:14 +02:00
|
|
|
// log.d("cached=${ response.cacheResponse() != null }")
|
2018-01-04 19:52:25 +01:00
|
|
|
|
2018-01-14 22:47:42 +01:00
|
|
|
try {
|
|
|
|
result.data = ProgressResponseBody.bytes(response) { bytesRead, bytesTotal ->
|
2018-01-04 19:52:25 +01:00
|
|
|
// 50MB以上のデータはキャンセルする
|
|
|
|
if(Math.max(bytesRead, bytesTotal) >= 50000000) {
|
|
|
|
throw RuntimeException("media attachment is larger than 50000000")
|
|
|
|
}
|
|
|
|
client.publishApiProgressRatio(bytesRead.toInt(), bytesTotal.toInt())
|
|
|
|
}
|
2018-01-21 13:46:36 +01:00
|
|
|
if(client.isApiCancelled) return null
|
2018-01-04 19:52:25 +01:00
|
|
|
} catch(ex : Throwable) {
|
2018-01-21 13:46:36 +01:00
|
|
|
result.setError(TootApiClient.formatResponse(response, result.caption, "?"))
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
2018-01-14 22:47:42 +01:00
|
|
|
return result
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
2018-01-14 22:47:42 +01:00
|
|
|
override fun background(client : TootApiClient) : TootApiResult? {
|
2018-01-21 13:46:36 +01:00
|
|
|
if(urlList.isEmpty()) return TootApiResult("missing url")
|
2018-01-14 22:47:42 +01:00
|
|
|
var result : TootApiResult? = null
|
|
|
|
for(url in urlList) {
|
|
|
|
result = getHttpCached(client, url)
|
|
|
|
val data = result?.data as? ByteArray
|
|
|
|
if(data != null) {
|
|
|
|
client.publishApiProgress("decoding image…")
|
|
|
|
val bitmap = decodeBitmap(data, 2048)
|
2018-01-21 13:46:36 +01:00
|
|
|
if(bitmap != null) {
|
2018-01-14 22:47:42 +01:00
|
|
|
this.bitmap = bitmap
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun handleResult(result : TootApiResult?) {
|
2018-01-10 16:47:35 +01:00
|
|
|
val bitmap = this.bitmap
|
2018-01-21 13:46:36 +01:00
|
|
|
if(bitmap != null) {
|
2018-01-04 19:52:25 +01:00
|
|
|
pbvImage.setBitmap(bitmap)
|
2018-01-21 13:46:36 +01:00
|
|
|
} else if(result != null) {
|
|
|
|
showToast(this@ActMediaViewer, true, result.error)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onClick(v : View) {
|
|
|
|
try {
|
|
|
|
when(v.id) {
|
|
|
|
|
|
|
|
R.id.btnPrevious -> loadDelta(- 1)
|
|
|
|
R.id.btnNext -> loadDelta(+ 1)
|
|
|
|
R.id.btnDownload -> download(media_list[idx])
|
2018-08-18 12:58:14 +02:00
|
|
|
|
|
|
|
// case R.id.btnBrowser:
|
|
|
|
// share( Intent.ACTION_VIEW, media_list.get( idx ) );
|
|
|
|
// break;
|
|
|
|
// case R.id.btnShare:
|
|
|
|
// share( Intent.ACTION_SEND, media_list.get( idx ) );
|
|
|
|
// break;
|
|
|
|
// case R.id.btnCopy:
|
|
|
|
// copy( media_list.get( idx ) );
|
|
|
|
// break;
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
R.id.btnMore -> more(media_list[idx])
|
|
|
|
}
|
|
|
|
} catch(ex : Throwable) {
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(this, ex, "action failed.")
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
internal class DownloadHistory(val time : Long, val url : String)
|
|
|
|
|
|
|
|
private fun download(ta : TootAttachmentLike) {
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
val permissionCheck = ContextCompat.checkSelfPermission(
|
|
|
|
this,
|
|
|
|
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
|
|
)
|
2018-01-04 19:52:25 +01:00
|
|
|
if(permissionCheck != PackageManager.PERMISSION_GRANTED) {
|
|
|
|
preparePermission()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
val downLoadManager = getSystemService(DOWNLOAD_SERVICE) as? DownloadManager
|
2018-01-04 19:52:25 +01:00
|
|
|
?: throw NotImplementedError("missing DownloadManager system service")
|
|
|
|
|
|
|
|
val url = if(ta is TootAttachment) {
|
|
|
|
ta.getLargeUrl(App1.pref)
|
|
|
|
} else {
|
|
|
|
null
|
|
|
|
} ?: return
|
|
|
|
|
|
|
|
// ボタン連打対策
|
|
|
|
run {
|
|
|
|
val now = SystemClock.elapsedRealtime()
|
|
|
|
|
|
|
|
// 期限切れの履歴を削除
|
|
|
|
val it = download_history_list.iterator()
|
|
|
|
while(it.hasNext()) {
|
|
|
|
val dh = it.next()
|
|
|
|
if(now - dh.time >= DOWNLOAD_REPEAT_EXPIRE) {
|
|
|
|
// この履歴は十分に古いので捨てる
|
|
|
|
it.remove()
|
|
|
|
} else if(url == dh.url) {
|
|
|
|
// 履歴に同じURLがあればエラーとする
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(this, false, R.string.dont_repeat_download_to_same_url)
|
2018-01-04 19:52:25 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 履歴の末尾に追加(履歴は古い順に並ぶ)
|
|
|
|
download_history_list.addLast(DownloadHistory(now, url))
|
|
|
|
}
|
|
|
|
|
|
|
|
var fileName : String? = null
|
|
|
|
|
|
|
|
try {
|
|
|
|
val pathSegments = Uri.parse(url).pathSegments
|
|
|
|
if(pathSegments != null) {
|
|
|
|
val size = pathSegments.size
|
|
|
|
for(i in size - 1 downTo 0) {
|
|
|
|
val s = pathSegments[i]
|
2018-01-21 13:46:36 +01:00
|
|
|
if(s?.isNotEmpty() == true) {
|
2018-01-04 19:52:25 +01:00
|
|
|
fileName = s
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
log.trace(ex)
|
|
|
|
}
|
|
|
|
|
|
|
|
if(fileName == null) {
|
|
|
|
fileName = url
|
|
|
|
.replaceFirst("https?://".toRegex(), "")
|
|
|
|
.replace("[^.\\w\\d]+".toRegex(), "-")
|
|
|
|
}
|
|
|
|
if(fileName.length >= 20) fileName = fileName.substring(fileName.length - 20)
|
|
|
|
|
|
|
|
val request = DownloadManager.Request(Uri.parse(url))
|
|
|
|
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
|
|
|
|
request.setTitle(fileName)
|
|
|
|
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE or DownloadManager.Request.NETWORK_WIFI)
|
|
|
|
//メディアスキャンを許可する
|
|
|
|
request.allowScanningByMediaScanner()
|
|
|
|
|
|
|
|
//ダウンロード中・ダウンロード完了時にも通知を表示する
|
|
|
|
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
|
|
|
|
|
|
|
downLoadManager.enqueue(request)
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(this, false, R.string.downloading)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun share(action : String, url : String) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
val intent = Intent(action)
|
|
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
|
|
if(action == Intent.ACTION_SEND) {
|
|
|
|
intent.type = "text/plain"
|
|
|
|
intent.putExtra(Intent.EXTRA_TEXT, url)
|
|
|
|
} else {
|
|
|
|
intent.data = Uri.parse(url)
|
|
|
|
}
|
|
|
|
|
|
|
|
startActivity(intent)
|
|
|
|
} catch(ex : Throwable) {
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(this, ex, "can't open app.")
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
internal fun copy(url : String) {
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
val cm = getSystemService(CLIPBOARD_SERVICE) as? ClipboardManager
|
2018-01-04 19:52:25 +01:00
|
|
|
?: throw NotImplementedError("missing ClipboardManager system service")
|
|
|
|
|
|
|
|
try {
|
|
|
|
//クリップボードに格納するItemを作成
|
|
|
|
val item = ClipData.Item(url)
|
|
|
|
|
|
|
|
val mimeType = arrayOfNulls<String>(1)
|
|
|
|
mimeType[0] = ClipDescription.MIMETYPE_TEXT_PLAIN
|
|
|
|
|
|
|
|
//クリップボードに格納するClipDataオブジェクトの作成
|
|
|
|
val cd = ClipData(ClipDescription("media URL", mimeType), item)
|
|
|
|
|
|
|
|
//クリップボードにデータを格納
|
|
|
|
cm.primaryClip = cd
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(this, false, R.string.url_is_copied)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
} catch(ex : Throwable) {
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(this, ex, "clipboard access failed.")
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
internal fun more(ta : TootAttachmentLike) {
|
|
|
|
val ad = ActionsDialog()
|
|
|
|
|
|
|
|
if(ta is TootAttachment) {
|
|
|
|
val url = ta.getLargeUrl(App1.pref) ?: return
|
|
|
|
|
|
|
|
ad.addAction(getString(R.string.open_in_browser)) { share(Intent.ACTION_VIEW, url) }
|
|
|
|
ad.addAction(getString(R.string.share_url)) { share(Intent.ACTION_SEND, url) }
|
|
|
|
ad.addAction(getString(R.string.copy_url)) { copy(url) }
|
|
|
|
|
|
|
|
addMoreMenu(ad, "url", ta.url, Intent.ACTION_VIEW)
|
|
|
|
addMoreMenu(ad, "remote_url", ta.remote_url, Intent.ACTION_VIEW)
|
|
|
|
addMoreMenu(ad, "preview_url", ta.preview_url, Intent.ACTION_VIEW)
|
|
|
|
addMoreMenu(ad, "text_url", ta.text_url, Intent.ACTION_VIEW)
|
|
|
|
|
|
|
|
} else if(ta is TootAttachmentMSP) {
|
|
|
|
val url = ta.preview_url
|
|
|
|
ad.addAction(getString(R.string.open_in_browser)) { share(Intent.ACTION_VIEW, url) }
|
|
|
|
ad.addAction(getString(R.string.share_url)) { share(Intent.ACTION_SEND, url) }
|
|
|
|
ad.addAction(getString(R.string.copy_url)) { copy(url) }
|
|
|
|
}
|
|
|
|
|
|
|
|
ad.show(this, null)
|
|
|
|
}
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
private fun addMoreMenu(
|
|
|
|
ad : ActionsDialog,
|
|
|
|
caption_prefix : String,
|
|
|
|
url : String?,
|
|
|
|
action : String
|
|
|
|
) {
|
|
|
|
if(url?.isEmpty() != false) return
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
val caption = getString(R.string.open_browser_of, caption_prefix)
|
|
|
|
|
|
|
|
ad.addAction(caption) {
|
|
|
|
try {
|
|
|
|
val intent = Intent(action, Uri.parse(url))
|
|
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
|
|
startActivity(intent)
|
|
|
|
} catch(ex : Throwable) {
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(this@ActMediaViewer, ex, "can't open app.")
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun preparePermission() {
|
|
|
|
if(Build.VERSION.SDK_INT >= 23) {
|
2018-01-21 13:46:36 +01:00
|
|
|
ActivityCompat.requestPermissions(
|
|
|
|
this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), PERMISSION_REQUEST_CODE
|
2018-01-04 19:52:25 +01:00
|
|
|
)
|
|
|
|
} else {
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(this, true, R.string.missing_permission_to_access_media)
|
2018-01-04 19:52:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onRequestPermissionsResult(
|
|
|
|
requestCode : Int, permissions : Array<String>, grantResults : IntArray
|
|
|
|
) {
|
|
|
|
when(requestCode) {
|
|
|
|
PERMISSION_REQUEST_CODE -> {
|
|
|
|
var bNotGranted = false
|
|
|
|
var i = 0
|
|
|
|
val ie = permissions.size
|
|
|
|
while(i < ie) {
|
|
|
|
if(grantResults[i] != PackageManager.PERMISSION_GRANTED) {
|
|
|
|
bNotGranted = true
|
|
|
|
}
|
|
|
|
++ i
|
|
|
|
}
|
|
|
|
if(bNotGranted) {
|
2018-01-21 13:46:36 +01:00
|
|
|
showToast(this, true, R.string.missing_permission_to_access_media)
|
2018-01-04 19:52:25 +01:00
|
|
|
} else {
|
|
|
|
download(media_list[idx])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|