?, val mime: String)
+}
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/AttachmentsActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/AttachmentsActivity.kt
new file mode 100644
index 000000000..3149badee
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/AttachmentsActivity.kt
@@ -0,0 +1,63 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.db.model.AttachmentsTypes
+import dev.ragnarok.fenrir.fragment.docs.DocsFragment
+import dev.ragnarok.fenrir.fragment.docs.DocsListPresenter
+import dev.ragnarok.fenrir.fragment.videos.IVideosListView
+import dev.ragnarok.fenrir.fragment.videos.VideosFragment
+import dev.ragnarok.fenrir.fragment.videos.VideosTabsFragment
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceProvider
+
+class AttachmentsActivity : NoMainActivity(), PlaceProvider {
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ var fragment: Fragment? = null
+ val type = (intent.extras ?: return).getInt(Extra.TYPE)
+ val accountId = (intent.extras ?: return).getInt(Extra.ACCOUNT_ID)
+ when (type) {
+ AttachmentsTypes.DOC -> fragment =
+ DocsFragment.newInstance(accountId, accountId, DocsListPresenter.ACTION_SELECT)
+ AttachmentsTypes.VIDEO -> fragment = VideosTabsFragment.newInstance(
+ accountId,
+ accountId,
+ IVideosListView.ACTION_SELECT
+ )
+ }
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter, R.anim.fragment_exit)
+ .replace(R.id.fragment, fragment ?: return)
+ .addToBackStack(null)
+ .commit()
+ }
+ }
+
+ override fun openPlace(place: Place) {
+ if (place.type == Place.VIDEO_ALBUM) {
+ val fragment: Fragment = VideosFragment.newInstance(place.safeArguments())
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(R.id.fragment, fragment)
+ .addToBackStack("video_album")
+ .commit()
+ }
+ }
+
+ companion object {
+
+ fun createIntent(context: Context, accountId: Int, type: Int): Intent {
+ return Intent(context, AttachmentsActivity::class.java)
+ .putExtra(Extra.TYPE, type)
+ .putExtra(Extra.ACCOUNT_ID, accountId)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/AudioSelectActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/AudioSelectActivity.kt
new file mode 100644
index 000000000..ce3752a08
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/AudioSelectActivity.kt
@@ -0,0 +1,65 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.fragment.audio.AudioSelectTabsFragment
+import dev.ragnarok.fenrir.fragment.audio.audios.AudiosFragment
+import dev.ragnarok.fenrir.fragment.search.SingleTabSearchFragment
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceProvider
+
+class AudioSelectActivity : NoMainActivity(), PlaceProvider {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ savedInstanceState ?: run {
+ val accountId = (intent.extras ?: return@run).getInt(Extra.ACCOUNT_ID)
+ attachInitialFragment(accountId)
+ }
+ }
+
+ private fun attachInitialFragment(accountId: Int) {
+ val fragment = AudioSelectTabsFragment.newInstance(accountId)
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(getMainContainerViewId(), fragment)
+ .addToBackStack("audio-select")
+ .commit()
+ }
+
+ override fun openPlace(place: Place) {
+ if (place.type == Place.SINGLE_SEARCH) {
+ val singleTabSearchFragment = SingleTabSearchFragment.newInstance(place.safeArguments())
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(getMainContainerViewId(), singleTabSearchFragment)
+ .addToBackStack("audio-search-select")
+ .commit()
+ } else if (place.type == Place.AUDIOS_IN_ALBUM) {
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(
+ getMainContainerViewId(),
+ AudiosFragment.newInstance(place.safeArguments(), true)
+ )
+ .addToBackStack("audio-in_playlist-select")
+ .commit()
+ }
+ }
+
+ companion object {
+ /**
+ * @param accountId От чьего имени получать
+ */
+
+ fun createIntent(context: Context, accountId: Int): Intent {
+ return Intent(context, AudioSelectActivity::class.java)
+ .putExtra(Extra.ACCOUNT_ID, accountId)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/BaseMvpActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/BaseMvpActivity.kt
new file mode 100644
index 000000000..d53d760e0
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/BaseMvpActivity.kt
@@ -0,0 +1,108 @@
+package dev.ragnarok.fenrir.activity
+
+import android.os.Bundle
+import android.view.View
+import android.widget.CompoundButton
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AlertDialog
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import dev.ragnarok.fenrir.Includes.provideApplicationContext
+import dev.ragnarok.fenrir.fragment.base.compat.AbsMvpActivity
+import dev.ragnarok.fenrir.fragment.base.core.AbsPresenter
+import dev.ragnarok.fenrir.fragment.base.core.IMvpView
+import dev.ragnarok.fenrir.service.ErrorLocalizer.localizeThrowable
+import dev.ragnarok.fenrir.util.ViewUtils
+import dev.ragnarok.fenrir.util.spots.SpotsDialog
+import dev.ragnarok.fenrir.util.toast.CustomToast
+import dev.ragnarok.fenrir.util.toast.CustomToast.Companion.createCustomToast
+
+abstract class BaseMvpActivity, V : IMvpView> : AbsMvpActivity
(),
+ IMvpView {
+ private var mLoadingProgressDialog: AlertDialog? = null
+ protected val arguments: Bundle?
+ get() = intent?.extras
+
+ protected fun requireArguments(): Bundle {
+ return intent!!.extras!!
+ }
+
+ override fun showError(errorText: String?) {
+ if (!isFinishing) {
+ customToast.showToastError(errorText)
+ }
+ }
+
+ override fun showThrowable(throwable: Throwable?) {
+ if (!isFinishing) {
+ showError(localizeThrowable(provideApplicationContext(), throwable))
+ }
+ }
+
+ override val customToast: CustomToast
+ get() = if (!isFinishing) {
+ createCustomToast(this)
+ } else createCustomToast(null)
+
+ override fun showError(@StringRes titleTes: Int, vararg params: Any?) {
+ if (!isFinishing) {
+ showError(getString(titleTes, *params))
+ }
+ }
+
+ override fun setToolbarSubtitle(subtitle: String?) {
+ supportActionBar?.subtitle = subtitle
+ }
+
+ override fun setToolbarTitle(title: String?) {
+ supportActionBar?.title = title
+ }
+
+ protected fun styleSwipeRefreshLayoutWithCurrentTheme(
+ swipeRefreshLayout: SwipeRefreshLayout,
+ needToolbarOffset: Boolean
+ ) {
+ ViewUtils.setupSwipeRefreshLayoutWithCurrentTheme(
+ this,
+ swipeRefreshLayout,
+ needToolbarOffset
+ )
+ }
+
+ override fun displayProgressDialog(
+ @StringRes title: Int,
+ @StringRes message: Int,
+ cancelable: Boolean
+ ) {
+ dismissProgressDialog()
+ mLoadingProgressDialog = SpotsDialog.Builder().setContext(this)
+ .setMessage(getString(title) + ": " + getString(message)).setCancelable(cancelable)
+ .build()
+ mLoadingProgressDialog?.show()
+ }
+
+ override fun dismissProgressDialog() {
+ if (mLoadingProgressDialog?.isShowing == true) {
+ mLoadingProgressDialog?.cancel()
+ }
+ }
+
+ companion object {
+ const val EXTRA_HIDE_TOOLBAR = "extra_hide_toolbar"
+ protected fun safelySetChecked(button: CompoundButton?, checked: Boolean) {
+ button?.isChecked = checked
+ }
+
+ protected fun safelySetText(target: TextView?, text: String?) {
+ target?.text = text
+ }
+
+ protected fun safelySetText(target: TextView?, @StringRes text: Int) {
+ target?.setText(text)
+ }
+
+ protected fun safelySetVisibleOrGone(target: View?, visible: Boolean) {
+ target?.visibility = if (visible) View.VISIBLE else View.GONE
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/CaptchaActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/CaptchaActivity.kt
new file mode 100644
index 000000000..a546c1106
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/CaptchaActivity.kt
@@ -0,0 +1,115 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.ImageView
+import androidx.activity.OnBackPressedCallback
+import androidx.annotation.StyleRes
+import androidx.appcompat.app.AppCompatActivity
+import com.google.android.material.textfield.TextInputEditText
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.Includes
+import dev.ragnarok.fenrir.Includes.provideMainThreadScheduler
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.api.ICaptchaProvider
+import dev.ragnarok.fenrir.picasso.PicassoInstance.Companion.with
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.settings.theme.ThemeOverlay
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.rxutils.RxUtils
+import dev.ragnarok.fenrir.util.toast.CustomToast
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+
+class CaptchaActivity : AppCompatActivity() {
+ private val mCompositeDisposable = CompositeDisposable()
+ private var mTextField: TextInputEditText? = null
+ private var captchaProvider: ICaptchaProvider? = null
+ private var requestSid: String? = null
+ override fun attachBaseContext(newBase: Context) {
+ super.attachBaseContext(Utils.updateActivityContext(newBase))
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ @StyleRes val theme: Int = when (Settings.get().main().themeOverlay) {
+ ThemeOverlay.AMOLED -> R.style.QuickReply_Amoled
+ ThemeOverlay.MD1 -> R.style.QuickReply_MD1
+ ThemeOverlay.OFF -> R.style.QuickReply
+ else -> R.style.QuickReply
+ }
+ setTheme(theme)
+ super.onCreate(savedInstanceState)
+ setFinishOnTouchOutside(false)
+ setContentView(R.layout.activity_captcha)
+ val imageView =
+ findViewById(R.id.captcha_view)
+ mTextField = findViewById(R.id.captcha_text)
+ val image = intent.getStringExtra(Extra.CAPTCHA_URL)
+
+ //onSuccess, w: 130, h: 50
+ with()
+ .load(image)
+ .into(imageView)
+ findViewById(R.id.button_cancel).setOnClickListener { cancel() }
+ findViewById(R.id.button_ok).setOnClickListener { onOkButtonClick() }
+ requestSid = intent.getStringExtra(Extra.CAPTCHA_SID)
+ captchaProvider = Includes.captchaProvider
+
+ captchaProvider?.let {
+ mCompositeDisposable.add(
+ it.observeWaiting()
+ .filter { ob -> ob == requestSid }
+ .observeOn(provideMainThreadScheduler())
+ .subscribe({ onWaitingRequestReceived() }, RxUtils.ignore())
+ )
+ mCompositeDisposable.add(
+ it.observeCanceling()
+ .filter { ob -> ob == requestSid }
+ .observeOn(provideMainThreadScheduler())
+ .subscribe({ onRequestCancelled() }, RxUtils.ignore())
+ )
+ }
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ }
+ })
+ }
+
+ private fun cancel() {
+ requestSid?.let { captchaProvider?.cancel(it) }
+ finish()
+ }
+
+ private fun onRequestCancelled() {
+ finish()
+ }
+
+ private fun onWaitingRequestReceived() {
+ requestSid?.let { captchaProvider?.notifyThatCaptchaEntryActive(it) }
+ }
+
+ override fun onDestroy() {
+ mCompositeDisposable.dispose()
+ super.onDestroy()
+ }
+
+ private fun onOkButtonClick() {
+ val text: CharSequence? = mTextField?.text
+ if (text.isNullOrEmpty()) {
+ CustomToast.createCustomToast(this).showToastError(R.string.enter_captcha_text)
+ return
+ }
+ requestSid?.let { captchaProvider?.enterCode(it, text.toString()) }
+ finish()
+ }
+
+ companion object {
+
+ fun createIntent(context: Context, captchaSid: String?, captchaImg: String?): Intent {
+ return Intent(context, CaptchaActivity::class.java)
+ .putExtra(Extra.CAPTCHA_SID, captchaSid)
+ .putExtra(Extra.CAPTCHA_URL, captchaImg)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ChatActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ChatActivity.kt
new file mode 100644
index 000000000..1b78cd932
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ChatActivity.kt
@@ -0,0 +1,190 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.view.View
+import android.view.WindowManager
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.ColorInt
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.gifpager.GifPagerActivity
+import dev.ragnarok.fenrir.activity.photopager.PhotoPagerActivity.Companion.newInstance
+import dev.ragnarok.fenrir.activity.slidr.Slidr.attach
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.storypager.StoryPagerActivity
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment.Companion.newInstance
+import dev.ragnarok.fenrir.fragment.messages.chat.ChatFragment.Companion.newInstance
+import dev.ragnarok.fenrir.getParcelableCompat
+import dev.ragnarok.fenrir.getParcelableExtraCompat
+import dev.ragnarok.fenrir.listener.AppStyleable
+import dev.ragnarok.fenrir.model.Document
+import dev.ragnarok.fenrir.model.Peer
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceProvider
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.ViewUtils
+
+class ChatActivity : NoMainActivity(), PlaceProvider, AppStyleable {
+ //resolveToolbarNavigationIcon();
+ private val mOnBackStackChangedListener =
+ FragmentManager.OnBackStackChangedListener { keyboardHide() }
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ attach(
+ this,
+ SlidrConfig.Builder().scrimColor(CurrentTheme.getColorBackground(this)).build()
+ )
+ if (savedInstanceState == null) {
+ handleIntent(intent)
+ supportFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener)
+ }
+ }
+
+ private fun handleIntent(intent: Intent?) {
+ if (intent == null) {
+ finish()
+ return
+ }
+ val action = intent.action
+ if (ACTION_OPEN_PLACE == action) {
+ val place: Place? = intent.getParcelableExtraCompat(Extra.PLACE)
+ if (place == null) {
+ finish()
+ return
+ }
+ openPlace(place)
+ }
+ }
+
+ fun keyboardHide() {
+ try {
+ val inputManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager?
+ inputManager?.hideSoftInputFromWindow(
+ window.decorView.rootView.windowToken,
+ InputMethodManager.HIDE_NOT_ALWAYS
+ )
+ } catch (ignored: Exception) {
+ }
+ }
+
+ override fun openPlace(place: Place) {
+ val args = place.safeArguments()
+ when (place.type) {
+ Place.CHAT -> {
+ val peer: Peer = args.getParcelableCompat(Extra.PEER) ?: return
+ val chatFragment =
+ newInstance(args.getInt(Extra.ACCOUNT_ID), args.getInt(Extra.OWNER_ID), peer)
+ attachToFront(chatFragment)
+ }
+ Place.VK_PHOTO_ALBUM_GALLERY, Place.FAVE_PHOTOS_GALLERY, Place.SIMPLE_PHOTO_GALLERY, Place.VK_PHOTO_TMP_SOURCE, Place.VK_PHOTO_ALBUM_GALLERY_SAVED, Place.VK_PHOTO_ALBUM_GALLERY_NATIVE -> newInstance(
+ this,
+ place.type,
+ args
+ )?.let {
+ place.launchActivityForResult(
+ this,
+ it
+ )
+ }
+ Place.STORY_PLAYER -> place.launchActivityForResult(
+ this,
+ StoryPagerActivity.newInstance(this, args)
+ )
+ Place.SINGLE_PHOTO -> place.launchActivityForResult(
+ this,
+ SinglePhotoActivity.newInstance(this, args)
+ )
+ Place.GIF_PAGER -> place.launchActivityForResult(
+ this,
+ GifPagerActivity.newInstance(this, args)
+ )
+ Place.DOC_PREVIEW -> {
+ val document: Document? = args.getParcelableCompat(Extra.DOC)
+ if (document != null && document.hasValidGifVideoLink()) {
+ val aid = args.getInt(Extra.ACCOUNT_ID)
+ val documents = ArrayList(listOf(document))
+ val extra = GifPagerActivity.buildArgs(aid, documents, 0)
+ place.launchActivityForResult(this, GifPagerActivity.newInstance(this, extra))
+ } else {
+ Utils.openPlaceWithSwipebleActivity(this, place)
+ }
+ }
+ Place.PLAYER -> {
+ val player = supportFragmentManager.findFragmentByTag("audio_player")
+ if (player is AudioPlayerFragment) player.dismiss()
+ newInstance(args).show(supportFragmentManager, "audio_player")
+ }
+ else -> Utils.openPlaceWithSwipebleActivity(this, place)
+ }
+ }
+
+ private fun attachToFront(fragment: Fragment, animate: Boolean = true) {
+ val fragmentTransaction = supportFragmentManager.beginTransaction()
+ if (animate) fragmentTransaction.setCustomAnimations(
+ R.anim.fragment_enter,
+ R.anim.fragment_exit
+ )
+ fragmentTransaction
+ .replace(R.id.fragment, fragment)
+ .addToBackStack(null)
+ .commitAllowingStateLoss()
+ }
+
+ public override fun onPause() {
+ ViewUtils.keyboardHide(this)
+ super.onPause()
+ }
+
+ public override fun onDestroy() {
+ supportFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener)
+ ViewUtils.keyboardHide(this)
+ super.onDestroy()
+ }
+
+ override fun hideMenu(hide: Boolean) {}
+ override fun openMenu(open: Boolean) {}
+
+ @Suppress("DEPRECATION")
+ override fun setStatusbarColored(colored: Boolean, invertIcons: Boolean) {
+ val statusbarNonColored = CurrentTheme.getStatusBarNonColored(this)
+ val statusbarColored = CurrentTheme.getStatusBarColor(this)
+ val w = window
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.statusBarColor = if (colored) statusbarColored else statusbarNonColored
+ @ColorInt val navigationColor =
+ if (colored) CurrentTheme.getNavigationBarColor(this) else Color.BLACK
+ w.navigationBarColor = navigationColor
+ if (Utils.hasMarshmallow()) {
+ var flags = window.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ }
+ window.decorView.systemUiVisibility = flags
+ }
+ if (Utils.hasOreo()) {
+ var flags = window.decorView.systemUiVisibility
+ if (invertIcons) {
+ flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ w.decorView.systemUiVisibility = flags
+ w.navigationBarColor = Color.WHITE
+ } else {
+ flags = flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ w.decorView.systemUiVisibility = flags
+ }
+ }
+ }
+
+ companion object {
+ const val ACTION_OPEN_PLACE = "dev.ragnarok.fenrir.activity.ChatActivity.openPlace"
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ChatActivityBubbles.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ChatActivityBubbles.kt
new file mode 100644
index 000000000..6bf14e9c3
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ChatActivityBubbles.kt
@@ -0,0 +1,223 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.view.View
+import android.view.WindowManager
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.ColorInt
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.gifpager.GifPagerActivity
+import dev.ragnarok.fenrir.activity.photopager.PhotoPagerActivity.Companion.newInstance
+import dev.ragnarok.fenrir.activity.slidr.Slidr.attach
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrListener
+import dev.ragnarok.fenrir.activity.storypager.StoryPagerActivity
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment.Companion.newInstance
+import dev.ragnarok.fenrir.fragment.messages.chat.ChatFragment.Companion.newInstance
+import dev.ragnarok.fenrir.getParcelableCompat
+import dev.ragnarok.fenrir.getParcelableExtraCompat
+import dev.ragnarok.fenrir.listener.AppStyleable
+import dev.ragnarok.fenrir.longpoll.NotificationHelper
+import dev.ragnarok.fenrir.model.Document
+import dev.ragnarok.fenrir.model.Peer
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceFactory
+import dev.ragnarok.fenrir.place.PlaceProvider
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.ViewUtils
+
+class ChatActivityBubbles : NoMainActivity(), PlaceProvider, AppStyleable {
+ //resolveToolbarNavigationIcon();
+ private val mOnBackStackChangedListener =
+ FragmentManager.OnBackStackChangedListener { keyboardHide() }
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ attach(this, SlidrConfig.Builder().listener(object : SlidrListener {
+ override fun onSlideStateChanged(state: Int) {}
+ override fun onSlideChange(percent: Float) {}
+ override fun onSlideOpened() {}
+ override fun onSlideClosed(): Boolean {
+ NotificationHelper.resetBubbleOpened(this@ChatActivityBubbles, false)
+ finish()
+ return true
+ }
+ }).scrimColor(CurrentTheme.getColorBackground(this)).build())
+ if (savedInstanceState == null) {
+ handleIntent(intent)
+ supportFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener)
+ }
+ }
+
+ private fun handleIntent(intent: Intent?) {
+ if (intent == null) {
+ finish()
+ return
+ }
+ val action = intent.action
+ if (ACTION_OPEN_PLACE == action) {
+ val place: Place? = intent.getParcelableExtraCompat(Extra.PLACE)
+ if (place == null) {
+ finish()
+ return
+ }
+ openPlace(place)
+ }
+ }
+
+ fun keyboardHide() {
+ try {
+ val inputManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager?
+ inputManager?.hideSoftInputFromWindow(
+ window.decorView.rootView.windowToken,
+ InputMethodManager.HIDE_NOT_ALWAYS
+ )
+ } catch (ignored: Exception) {
+ }
+ }
+
+ override fun openPlace(place: Place) {
+ val args = place.safeArguments()
+ when (place.type) {
+ Place.CHAT -> {
+ val peer: Peer = args.getParcelableCompat(Extra.PEER) ?: return
+ val chatFragment =
+ newInstance(args.getInt(Extra.ACCOUNT_ID), args.getInt(Extra.OWNER_ID), peer)
+ attachToFront(chatFragment)
+ }
+ Place.VK_PHOTO_ALBUM_GALLERY, Place.FAVE_PHOTOS_GALLERY, Place.SIMPLE_PHOTO_GALLERY, Place.VK_PHOTO_TMP_SOURCE, Place.VK_PHOTO_ALBUM_GALLERY_SAVED, Place.VK_PHOTO_ALBUM_GALLERY_NATIVE -> newInstance(
+ this,
+ place.type,
+ args
+ )?.let {
+ place.launchActivityForResult(
+ this,
+ it
+ )
+ }
+ Place.SINGLE_PHOTO -> place.launchActivityForResult(
+ this,
+ SinglePhotoActivity.newInstance(this, args)
+ )
+ Place.STORY_PLAYER -> place.launchActivityForResult(
+ this,
+ StoryPagerActivity.newInstance(this, args)
+ )
+ Place.GIF_PAGER -> place.launchActivityForResult(
+ this,
+ GifPagerActivity.newInstance(this, args)
+ )
+ Place.DOC_PREVIEW -> {
+ val document: Document? = args.getParcelableCompat(Extra.DOC)
+ if (document != null && document.hasValidGifVideoLink()) {
+ val aid = args.getInt(Extra.ACCOUNT_ID)
+ val documents = ArrayList(listOf(document))
+ val extra = GifPagerActivity.buildArgs(aid, documents, 0)
+ place.launchActivityForResult(this, GifPagerActivity.newInstance(this, extra))
+ } else {
+ Utils.openPlaceWithSwipebleActivity(this, place)
+ }
+ }
+ Place.PLAYER -> {
+ val player = supportFragmentManager.findFragmentByTag("audio_player")
+ if (player is AudioPlayerFragment) player.dismiss()
+ newInstance(args).show(supportFragmentManager, "audio_player")
+ }
+ else -> Utils.openPlaceWithSwipebleActivity(this, place)
+ }
+ }
+
+ private fun attachToFront(fragment: Fragment, animate: Boolean = true) {
+ val fragmentTransaction = supportFragmentManager.beginTransaction()
+ if (animate) fragmentTransaction.setCustomAnimations(
+ R.anim.fragment_enter,
+ R.anim.fragment_exit
+ )
+ fragmentTransaction
+ .replace(R.id.fragment, fragment)
+ .addToBackStack(null)
+ .commitAllowingStateLoss()
+ }
+
+ public override fun onPause() {
+ ViewUtils.keyboardHide(this)
+ super.onPause()
+ }
+
+ public override fun onResume() {
+ val data = intent
+ if (data != null && data.extras != null) {
+ NotificationHelper.setBubbleOpened(
+ (data.extras ?: return).getInt(Extra.ACCOUNT_ID),
+ (data.extras ?: return).getInt(Extra.OWNER_ID)
+ )
+ }
+ super.onResume()
+ }
+
+ public override fun onDestroy() {
+ NotificationHelper.resetBubbleOpened(this, true)
+ supportFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener)
+ ViewUtils.keyboardHide(this)
+ super.onDestroy()
+ }
+
+ override fun hideMenu(hide: Boolean) {}
+ override fun openMenu(open: Boolean) {}
+
+ @Suppress("DEPRECATION")
+ override fun setStatusbarColored(colored: Boolean, invertIcons: Boolean) {
+ val statusbarNonColored = CurrentTheme.getStatusBarNonColored(this)
+ val statusbarColored = CurrentTheme.getStatusBarColor(this)
+ val w = window
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.statusBarColor = if (colored) statusbarColored else statusbarNonColored
+ @ColorInt val navigationColor =
+ if (colored) CurrentTheme.getNavigationBarColor(this) else Color.BLACK
+ w.navigationBarColor = navigationColor
+ if (Utils.hasMarshmallow()) {
+ var flags = window.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ }
+ window.decorView.systemUiVisibility = flags
+ }
+ if (Utils.hasOreo()) {
+ var flags = window.decorView.systemUiVisibility
+ if (invertIcons) {
+ flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ w.decorView.systemUiVisibility = flags
+ w.navigationBarColor = Color.WHITE
+ } else {
+ flags = flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ w.decorView.systemUiVisibility = flags
+ }
+ }
+ }
+
+ companion object {
+ const val ACTION_OPEN_PLACE = "dev.ragnarok.fenrir.activity.ChatActivityBubbles.openPlace"
+
+
+ fun forStart(context: Context?, accountId: Int, peer: Peer): Intent {
+ val intent = Intent(context, ChatActivityBubbles::class.java)
+ intent.action = ACTION_OPEN_PLACE
+ intent.putExtra(Extra.PLACE, PlaceFactory.getChatPlace(accountId, accountId, peer))
+ intent.putExtra(Extra.ACCOUNT_ID, accountId)
+ intent.putExtra(Extra.OWNER_ID, peer.id)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ return intent
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/CreatePinActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/CreatePinActivity.kt
new file mode 100644
index 000000000..2a310da71
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/CreatePinActivity.kt
@@ -0,0 +1,17 @@
+package dev.ragnarok.fenrir.activity
+
+import android.os.Bundle
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.fragment.createpin.CreatePinFragment
+
+class CreatePinActivity : NoMainActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.fragment, CreatePinFragment.newInstance())
+ .commit()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/DeltaOwnerActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/DeltaOwnerActivity.kt
new file mode 100644
index 000000000..949232867
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/DeltaOwnerActivity.kt
@@ -0,0 +1,317 @@
+package dev.ragnarok.fenrir.activity
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.google.android.material.tabs.TabLayout
+import com.google.android.material.tabs.TabLayoutMediator
+import dev.ragnarok.fenrir.*
+import dev.ragnarok.fenrir.activity.slidr.Slidr.attach
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrPosition
+import dev.ragnarok.fenrir.fragment.absownerslist.OwnersAdapter
+import dev.ragnarok.fenrir.listener.AppStyleable
+import dev.ragnarok.fenrir.model.DeltaOwner
+import dev.ragnarok.fenrir.model.Owner
+import dev.ragnarok.fenrir.picasso.PicassoInstance
+import dev.ragnarok.fenrir.picasso.transforms.RoundTransformation
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceFactory
+import dev.ragnarok.fenrir.place.PlaceProvider
+import dev.ragnarok.fenrir.push.OwnerInfo
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.settings.theme.ThemesController
+import dev.ragnarok.fenrir.util.AppTextUtils.getDateFromUnixTime
+import dev.ragnarok.fenrir.util.DownloadWorkUtils
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.serializeble.json.Json
+import dev.ragnarok.fenrir.util.serializeble.json.decodeFromStream
+import dev.ragnarok.fenrir.util.toast.CustomToast
+import io.reactivex.rxjava3.disposables.Disposable
+import java.io.File
+import java.io.FileOutputStream
+import java.nio.charset.StandardCharsets
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+
+class DeltaOwnerActivity : AppCompatActivity(), PlaceProvider, AppStyleable {
+ private var mToolbar: Toolbar? = null
+ private var disposable: Disposable = Disposable.disposed()
+ private val DOWNLOAD_DATE_FORMAT: DateFormat =
+ SimpleDateFormat("yyyyMMdd_HHmmss", Utils.appLocale)
+
+ @Suppress("DEPRECATION")
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ setTheme(ThemesController.currentStyle())
+ Utils.prepareDensity(this)
+ super.onCreate(savedInstanceState)
+ attach(
+ this,
+ SlidrConfig.Builder().fromUnColoredToColoredStatusBar(true)
+ .position(SlidrPosition.LEFT).scrimColor(CurrentTheme.getColorBackground(this))
+ .build()
+ )
+ setContentView(R.layout.activity_delta_owner)
+ setSupportActionBar(findViewById(R.id.toolbar))
+ supportActionBar?.title = null
+ supportActionBar?.subtitle = null
+
+ val Export: FloatingActionButton = findViewById(R.id.delta_export)
+
+ val action = intent.action
+ val delta: DeltaOwner = if (Intent.ACTION_VIEW == action) {
+ try {
+ Export.visibility = View.GONE
+ intent.data?.let { uri ->
+ contentResolver.openInputStream(
+ uri
+ )?.let {
+ val s = kJson.decodeFromStream(DeltaOwner.serializer(), it)
+ it.close()
+ s
+ }
+ } ?: DeltaOwner()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ CustomToast.createCustomToast(this).showToastError(e.localizedMessage)
+ DeltaOwner()
+ }
+ } else {
+ Export.visibility = View.VISIBLE
+ intent.extras?.getParcelableCompat(Extra.LIST) ?: DeltaOwner()
+ }
+
+ val accountId = intent.extras?.getInt(Extra.ACCOUNT_ID, Settings.get().accounts().current)
+ ?: Settings.get().accounts().current
+
+ val Title: TextView = findViewById(R.id.delta_title)
+ val Time: TextView = findViewById(R.id.delta_time)
+ val Avatar: ImageView = findViewById(R.id.toolbar_avatar)
+ val EmptyAvatar: TextView = findViewById(R.id.empty_avatar_text)
+
+ Time.text = getDateFromUnixTime(this, delta.time)
+
+ disposable = OwnerInfo.getRx(this, accountId, delta.ownerId)
+ .fromIOToMain()
+ .subscribe({ owner ->
+ Export.setOnClickListener {
+ DownloadWorkUtils.CheckDirectory(Settings.get().other().docDir)
+ val file = File(
+ Settings.get().other().docDir, DownloadWorkUtils.makeLegalFilename(
+ "OwnerChanges_" + owner.owner.fullName.orEmpty() + "_" + DOWNLOAD_DATE_FORMAT.format(
+ delta.time * 1000L
+ ), "json"
+ )
+ )
+ var out: FileOutputStream? = null
+ try {
+ val bytes = Json { prettyPrint = true }.encodeToString(
+ DeltaOwner.serializer(),
+ delta
+ ).toByteArray(
+ StandardCharsets.UTF_8
+ )
+ out = FileOutputStream(file)
+ val bom = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte())
+ out.write(bom)
+ out.write(bytes)
+ out.flush()
+ Includes.provideApplicationContext().sendBroadcast(
+ Intent(
+ Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
+ Uri.fromFile(file)
+ )
+ )
+ CustomToast.createCustomToast(this).showToast(
+ R.string.saved_to_param_file_name,
+ file.absolutePath
+ )
+ } catch (e: Exception) {
+ e.printStackTrace()
+ CustomToast.createCustomToast(this).showToastError(e.localizedMessage)
+ } finally {
+ Utils.safelyClose(out)
+ }
+ }
+
+ Avatar.setOnClickListener {
+ PlaceFactory.getOwnerWallPlace(accountId, owner.owner).tryOpenWith(this)
+ }
+ if (owner.owner.maxSquareAvatar.nonNullNoEmpty()) {
+ EmptyAvatar.visibility = View.GONE
+ Avatar.let {
+ PicassoInstance.with()
+ .load(owner.owner.maxSquareAvatar)
+ .transform(RoundTransformation())
+ .into(it)
+ }
+ } else {
+ Avatar.let { PicassoInstance.with().cancelRequest(it) }
+ if (owner.owner.fullName.nonNullNoEmpty()) {
+ EmptyAvatar.visibility = View.VISIBLE
+ var name: String = owner.owner.fullName.orEmpty()
+ if (name.length > 2) name = name.substring(0, 2)
+ name = name.trim { it <= ' ' }
+ EmptyAvatar.text = name
+ } else {
+ EmptyAvatar.visibility = View.GONE
+ }
+ Avatar.setImageBitmap(
+ RoundTransformation().localTransform(
+ Utils.createGradientChatImage(
+ 200,
+ 200,
+ owner.owner.ownerId.orZero()
+ )
+ )
+ )
+ }
+ Title.text = owner.owner.fullName
+ }, { CustomToast.createCustomToast(this).showToastThrowable(it) }
+ )
+
+ val viewPager: ViewPager2 = findViewById(R.id.delta_pager)
+ viewPager.offscreenPageLimit = 1
+ viewPager.setPageTransformer(
+ Utils.createPageTransform(
+ Settings.get().main().viewpager_page_transform
+ )
+ )
+ val adapter = Adapter(delta, accountId)
+ viewPager.adapter = adapter
+ TabLayoutMediator(
+ findViewById(R.id.delta_tabs),
+ viewPager
+ ) { tab: TabLayout.Tab, position: Int ->
+ tab.text = adapter.DeltaOwner.content[position].name
+ }.attach()
+
+ val w = window
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.statusBarColor = CurrentTheme.getStatusBarColor(this)
+ w.navigationBarColor = CurrentTheme.getNavigationBarColor(this)
+ }
+
+ private class RecyclerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val ivRecycler: RecyclerView = view.findViewById(R.id.alert_recycle)
+ }
+
+ private inner class Adapter(val DeltaOwner: DeltaOwner, private val accountId: Int) :
+ RecyclerView.Adapter() {
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onCreateViewHolder(container: ViewGroup, viewType: Int): RecyclerViewHolder {
+ return RecyclerViewHolder(
+ LayoutInflater.from(container.context)
+ .inflate(R.layout.recycle_frame, container, false)
+ )
+ }
+
+ override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
+ val list = DeltaOwner.content[position]
+ val adapter = OwnersAdapter(this@DeltaOwnerActivity, list.ownerList)
+ adapter.setClickListener(object : OwnersAdapter.ClickListener {
+ override fun onOwnerClick(owner: Owner) {
+ PlaceFactory.getOwnerWallPlace(accountId, owner)
+ .tryOpenWith(this@DeltaOwnerActivity)
+ }
+ })
+ holder.ivRecycler.layoutManager =
+ LinearLayoutManager(this@DeltaOwnerActivity, RecyclerView.VERTICAL, false)
+ holder.ivRecycler.adapter = adapter
+ }
+
+ override fun getItemCount(): Int {
+ return DeltaOwner.content.size
+ }
+ }
+
+ override fun attachBaseContext(newBase: Context) {
+ super.attachBaseContext(Utils.updateActivityContext(newBase))
+ }
+
+ override fun openPlace(place: Place) {
+ Utils.openPlaceWithSwipebleActivity(this, place)
+ }
+
+ override fun hideMenu(hide: Boolean) {}
+ override fun openMenu(open: Boolean) {}
+
+ override fun setSupportActionBar(toolbar: Toolbar?) {
+ super.setSupportActionBar(toolbar)
+ mToolbar = toolbar
+ resolveToolbarNavigationIcon()
+ }
+
+ private fun resolveToolbarNavigationIcon() {
+ mToolbar?.setNavigationIcon(R.drawable.close)
+ mToolbar?.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ disposable.dispose()
+ }
+
+ @Suppress("DEPRECATION")
+ override fun setStatusbarColored(colored: Boolean, invertIcons: Boolean) {
+ val statusbarNonColored = CurrentTheme.getStatusBarNonColored(this)
+ val statusbarColored = CurrentTheme.getStatusBarColor(this)
+ val w = window
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.statusBarColor = if (colored) statusbarColored else statusbarNonColored
+ @ColorInt val navigationColor =
+ if (colored) CurrentTheme.getNavigationBarColor(this) else Color.BLACK
+ w.navigationBarColor = navigationColor
+ if (Utils.hasMarshmallow()) {
+ var flags = window.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ }
+ window.decorView.systemUiVisibility = flags
+ }
+ if (Utils.hasOreo()) {
+ var flags = window.decorView.systemUiVisibility
+ if (invertIcons) {
+ flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ w.decorView.systemUiVisibility = flags
+ w.navigationBarColor = Color.WHITE
+ } else {
+ flags = flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ w.decorView.systemUiVisibility = flags
+ }
+ }
+ }
+
+ companion object {
+ fun showDeltaActivity(context: Context, accountId: Int, delta: DeltaOwner) {
+ if (delta.content.isEmpty()) {
+ return
+ }
+ val intent = Intent(context, DeltaOwnerActivity::class.java)
+ intent.putExtra(Extra.LIST, delta)
+ intent.putExtra(Extra.ACCOUNT_ID, accountId)
+ context.startActivity(intent)
+ }
+ }
+}
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/DualTabPhotoActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/DualTabPhotoActivity.kt
new file mode 100644
index 000000000..c318a83bd
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/DualTabPhotoActivity.kt
@@ -0,0 +1,100 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.fragment.DualTabPhotosFragment
+import dev.ragnarok.fenrir.fragment.localphotos.LocalPhotosFragment
+import dev.ragnarok.fenrir.fragment.vkphotos.IVkPhotosView
+import dev.ragnarok.fenrir.fragment.vkphotos.VKPhotosFragment
+import dev.ragnarok.fenrir.getParcelableCompat
+import dev.ragnarok.fenrir.getParcelableExtraCompat
+import dev.ragnarok.fenrir.model.LocalImageAlbum
+import dev.ragnarok.fenrir.model.selection.Sources
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceProvider
+
+class DualTabPhotoActivity : NoMainActivity(), PlaceProvider {
+ private var mMaxSelectionCount = 0
+ private var mSources: Sources? = null
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ mMaxSelectionCount = intent.getIntExtra(Extra.MAX_COUNT, 10)
+ mSources = intent.getParcelableExtraCompat(Extra.SOURCES)
+ attachStartFragment()
+ } else {
+ mMaxSelectionCount = savedInstanceState.getInt("mMaxSelectionCount")
+ mSources = savedInstanceState.getParcelableCompat("mSources")
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putInt("mMaxSelectionCount", mMaxSelectionCount)
+ outState.putParcelable("mSources", mSources)
+ }
+
+ private fun attachStartFragment() {
+ val fragment = DualTabPhotosFragment.newInstance(mSources)
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(getMainContainerViewId(), fragment)
+ .addToBackStack("dual-tab-photos")
+ .commit()
+ }
+
+ override fun openPlace(place: Place) {
+ val args = place.safeArguments()
+ when (place.type) {
+ Place.VK_PHOTO_ALBUM -> {
+ val albumId = args.getInt(Extra.ALBUM_ID)
+ val accountId = args.getInt(Extra.ACCOUNT_ID)
+ val ownerId = args.getInt(Extra.OWNER_ID)
+ val fragment = VKPhotosFragment.newInstance(
+ accountId,
+ ownerId,
+ albumId,
+ IVkPhotosView.ACTION_SELECT_PHOTOS
+ )
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(R.id.fragment, fragment)
+ .addToBackStack("vk-album-photos")
+ .commit()
+ }
+ Place.VK_INTERNAL_PLAYER -> {
+ val intent = Intent(this, VideoPlayerActivity::class.java)
+ intent.putExtras(args)
+ startActivity(intent)
+ }
+ Place.SINGLE_PHOTO -> {
+ place.launchActivityForResult(this, SinglePhotoActivity.newInstance(this, args))
+ }
+ Place.LOCAL_IMAGE_ALBUM -> {
+ val album: LocalImageAlbum? = args.getParcelableCompat(Extra.ALBUM)
+ val localPhotosFragment =
+ LocalPhotosFragment.newInstance(mMaxSelectionCount, album, false)
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(R.id.fragment, localPhotosFragment)
+ .addToBackStack("local-album-photos")
+ .commit()
+ }
+ }
+ }
+
+ companion object {
+
+ fun createIntent(context: Context, maxSelectionCount: Int, sources: Sources): Intent {
+ return Intent(context, DualTabPhotoActivity::class.java)
+ .putExtra(Extra.MAX_COUNT, maxSelectionCount)
+ .putExtra(Extra.SOURCES, sources)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/EnterPinActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/EnterPinActivity.kt
new file mode 100644
index 000000000..215ab5532
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/EnterPinActivity.kt
@@ -0,0 +1,26 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.os.Bundle
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.fragment.enterpin.EnterPinFragment
+import dev.ragnarok.fenrir.util.Utils
+
+open class EnterPinActivity : NoMainActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.fragment, EnterPinFragment.newInstance())
+ .commit()
+ }
+ }
+
+ companion object {
+
+ fun getClass(context: Context): Class<*> {
+ return if (Utils.is600dp(context)) EnterPinActivity::class.java else EnterPinActivityPortraitOnly::class.java
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/EnterPinActivityPortraitOnly.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/EnterPinActivityPortraitOnly.kt
new file mode 100644
index 000000000..2b54b4d19
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/EnterPinActivityPortraitOnly.kt
@@ -0,0 +1,3 @@
+package dev.ragnarok.fenrir.activity
+
+class EnterPinActivityPortraitOnly : EnterPinActivity()
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/FileManagerSelectActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/FileManagerSelectActivity.kt
new file mode 100644
index 000000000..7812ac4d8
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/FileManagerSelectActivity.kt
@@ -0,0 +1,58 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.slidr.Slidr
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.fragment.filemanagerselect.FileManagerSelectFragment
+import dev.ragnarok.fenrir.settings.CurrentTheme
+
+class FileManagerSelectActivity : NoMainActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Slidr.attach(
+ this,
+ SlidrConfig.Builder().scrimColor(CurrentTheme.getColorBackground(this)).build()
+ )
+ if (savedInstanceState == null) {
+ attachFragment()
+ }
+ }
+
+ private fun attachFragment() {
+ val args = Bundle()
+ args.putString(Extra.PATH, intent.extras?.getString(Extra.PATH))
+ args.putString(Extra.EXT, intent.extras?.getString(Extra.EXT))
+ if (intent.extras?.containsKey(Extra.TITLE) == true) {
+ args.putString(Extra.TITLE, intent.extras?.getString(Extra.TITLE))
+ }
+ val fileManagerFragment = FileManagerSelectFragment()
+ fileManagerFragment.arguments = args
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.fragment, fileManagerFragment)
+ .commit()
+ }
+
+ companion object {
+ fun makeFileManager(context: Context, path: String, ext: String?): Intent {
+ val intent = Intent(context, FileManagerSelectActivity::class.java)
+ intent.putExtra(Extra.PATH, path)
+ intent.putExtra(Extra.EXT, ext)
+ return intent
+ }
+
+ fun makeFileManager(context: Context, path: String, ext: String?, header: String?): Intent {
+ val intent = Intent(context, FileManagerSelectActivity::class.java)
+ intent.putExtra(Extra.PATH, path)
+ intent.putExtra(Extra.EXT, ext)
+ header?.let {
+ intent.putExtra(Extra.TITLE, it)
+ }
+ return intent
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/KeyExchangeCommitActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/KeyExchangeCommitActivity.kt
new file mode 100644
index 000000000..c94002563
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/KeyExchangeCommitActivity.kt
@@ -0,0 +1,97 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.StyleRes
+import androidx.appcompat.app.AppCompatActivity
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.crypt.ExchangeMessage
+import dev.ragnarok.fenrir.crypt.KeyExchangeService
+import dev.ragnarok.fenrir.getParcelableExtraCompat
+import dev.ragnarok.fenrir.model.User
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.settings.theme.ThemeOverlay
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.ViewUtils
+
+class KeyExchangeCommitActivity : AppCompatActivity() {
+ override fun attachBaseContext(newBase: Context) {
+ super.attachBaseContext(Utils.updateActivityContext(newBase))
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ @StyleRes val theme: Int = when (Settings.get().main().themeOverlay) {
+ ThemeOverlay.AMOLED -> R.style.QuickReply_Amoled
+ ThemeOverlay.MD1 -> R.style.QuickReply_MD1
+ ThemeOverlay.OFF -> R.style.QuickReply
+ else -> R.style.QuickReply
+ }
+ setTheme(theme)
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_key_exchange_commit)
+ val accountId = (intent.extras ?: return).getInt(Extra.ACCOUNT_ID)
+ val peerId = (intent.extras ?: return).getInt(Extra.PEER_ID)
+ val user: User = intent.getParcelableExtraCompat(Extra.OWNER) ?: return
+ val messageId = (intent.extras ?: return).getInt(Extra.MESSAGE_ID)
+ val message: ExchangeMessage = intent.getParcelableExtraCompat(Extra.MESSAGE) ?: return
+ val avatar = findViewById(R.id.avatar)
+ ViewUtils.displayAvatar(
+ avatar,
+ CurrentTheme.createTransformationForAvatar(),
+ user.maxSquareAvatar,
+ null
+ )
+ val userName = findViewById(R.id.user_name)
+ userName.text = user.fullName
+ findViewById(R.id.accept_button).setOnClickListener {
+ startService(
+ KeyExchangeService.createIntentForApply(
+ this,
+ message,
+ accountId,
+ peerId,
+ messageId
+ )
+ )
+ finish()
+ }
+ findViewById(R.id.decline_button).setOnClickListener {
+ startService(
+ KeyExchangeService.createIntentForDecline(
+ this,
+ message,
+ accountId,
+ peerId,
+ messageId
+ )
+ )
+ finish()
+ }
+ }
+
+ companion object {
+
+ fun createIntent(
+ context: Context,
+ accountId: Int,
+ peerId: Int,
+ user: User,
+ messageId: Int,
+ message: ExchangeMessage
+ ): Intent {
+ val intent = Intent(context, KeyExchangeCommitActivity::class.java)
+ intent.putExtra(Extra.ACCOUNT_ID, accountId)
+ intent.putExtra(Extra.OWNER, user)
+ intent.putExtra(Extra.PEER_ID, peerId)
+ intent.putExtra(Extra.MESSAGE_ID, messageId)
+ intent.putExtra(Extra.MESSAGE, message)
+ return intent
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/LocalJsonToChatActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/LocalJsonToChatActivity.kt
new file mode 100644
index 000000000..26114c375
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/LocalJsonToChatActivity.kt
@@ -0,0 +1,175 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.view.View
+import android.view.WindowManager
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.ColorInt
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.gifpager.GifPagerActivity
+import dev.ragnarok.fenrir.activity.photopager.PhotoPagerActivity.Companion.newInstance
+import dev.ragnarok.fenrir.activity.storypager.StoryPagerActivity
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment.Companion.newInstance
+import dev.ragnarok.fenrir.fragment.messages.localjsontochat.LocalJsonToChatFragment
+import dev.ragnarok.fenrir.getParcelableCompat
+import dev.ragnarok.fenrir.listener.AppStyleable
+import dev.ragnarok.fenrir.model.Document
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceProvider
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.ISettings
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.ViewUtils
+
+class LocalJsonToChatActivity : NoMainActivity(), PlaceProvider, AppStyleable {
+ private val mOnBackStackChangedListener =
+ FragmentManager.OnBackStackChangedListener { keyboardHide() }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ handleIntent(intent)
+ supportFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener)
+ }
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ handleIntent(intent)
+ }
+
+ private fun handleIntent(intent: Intent?) {
+ if (intent == null) {
+ finish()
+ return
+ }
+ val accountId = Settings.get().accounts().current
+ if (accountId == ISettings.IAccountsSettings.INVALID_ID) {
+ finish()
+ return
+ }
+ val action = intent.action
+ if (Intent.ACTION_VIEW == action) {
+ attachInitialFragment(LocalJsonToChatFragment.newInstance(accountId))
+ }
+ }
+
+ fun keyboardHide() {
+ try {
+ val inputManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager?
+ inputManager?.hideSoftInputFromWindow(
+ window.decorView.rootView.windowToken,
+ InputMethodManager.HIDE_NOT_ALWAYS
+ )
+ } catch (ignored: Exception) {
+ }
+ }
+
+ private fun attachInitialFragment(fragment: Fragment) {
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter, R.anim.fragment_exit)
+ .replace(getMainContainerViewId(), fragment)
+ .addToBackStack("primary_local_chat")
+ .commitAllowingStateLoss()
+ }
+
+ override fun openPlace(place: Place) {
+ val args = place.safeArguments()
+ when (place.type) {
+ Place.VK_PHOTO_ALBUM_GALLERY, Place.FAVE_PHOTOS_GALLERY, Place.SIMPLE_PHOTO_GALLERY, Place.VK_PHOTO_TMP_SOURCE, Place.VK_PHOTO_ALBUM_GALLERY_SAVED, Place.VK_PHOTO_ALBUM_GALLERY_NATIVE -> newInstance(
+ this,
+ place.type,
+ args
+ )?.let {
+ place.launchActivityForResult(
+ this,
+ it
+ )
+ }
+ Place.STORY_PLAYER -> place.launchActivityForResult(
+ this,
+ StoryPagerActivity.newInstance(this, args)
+ )
+ Place.SINGLE_PHOTO -> place.launchActivityForResult(
+ this,
+ SinglePhotoActivity.newInstance(this, args)
+ )
+ Place.GIF_PAGER -> place.launchActivityForResult(
+ this,
+ GifPagerActivity.newInstance(this, args)
+ )
+ Place.DOC_PREVIEW -> {
+ val document: Document? = args.getParcelableCompat(Extra.DOC)
+ if (document != null && document.hasValidGifVideoLink()) {
+ val aid = args.getInt(Extra.ACCOUNT_ID)
+ val documents = ArrayList(listOf(document))
+ val extra = GifPagerActivity.buildArgs(aid, documents, 0)
+ place.launchActivityForResult(this, GifPagerActivity.newInstance(this, extra))
+ } else {
+ Utils.openPlaceWithSwipebleActivity(this, place)
+ }
+ }
+ Place.PLAYER -> {
+ val player = supportFragmentManager.findFragmentByTag("audio_player")
+ if (player is AudioPlayerFragment) player.dismiss()
+ newInstance(args).show(supportFragmentManager, "audio_player")
+ }
+ else -> Utils.openPlaceWithSwipebleActivity(this, place)
+ }
+ }
+
+ public override fun onPause() {
+ ViewUtils.keyboardHide(this)
+ super.onPause()
+ }
+
+ public override fun onDestroy() {
+ supportFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener)
+ ViewUtils.keyboardHide(this)
+ super.onDestroy()
+ }
+
+ override fun hideMenu(hide: Boolean) {}
+ override fun openMenu(open: Boolean) {}
+
+ @Suppress("DEPRECATION")
+ override fun setStatusbarColored(colored: Boolean, invertIcons: Boolean) {
+ val statusbarNonColored = CurrentTheme.getStatusBarNonColored(this)
+ val statusbarColored = CurrentTheme.getStatusBarColor(this)
+ val w = window
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.statusBarColor = if (colored) statusbarColored else statusbarNonColored
+ @ColorInt val navigationColor =
+ if (colored) CurrentTheme.getNavigationBarColor(this) else Color.BLACK
+ w.navigationBarColor = navigationColor
+ if (Utils.hasMarshmallow()) {
+ var flags = window.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ }
+ window.decorView.systemUiVisibility = flags
+ }
+ if (Utils.hasOreo()) {
+ var flags = window.decorView.systemUiVisibility
+ if (invertIcons) {
+ flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ w.decorView.systemUiVisibility = flags
+ w.navigationBarColor = Color.WHITE
+ } else {
+ flags = flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ w.decorView.systemUiVisibility = flags
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/LoginActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/LoginActivity.kt
new file mode 100644
index 000000000..f4d31c9e8
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/LoginActivity.kt
@@ -0,0 +1,191 @@
+package dev.ragnarok.fenrir.activity
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.os.Bundle
+import android.util.Log
+import android.webkit.CookieManager
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.appcompat.app.AppCompatActivity
+import dev.ragnarok.fenrir.*
+import dev.ragnarok.fenrir.Constants.USER_AGENT
+import dev.ragnarok.fenrir.api.Auth
+import dev.ragnarok.fenrir.api.util.VKStringUtils
+import dev.ragnarok.fenrir.model.Token
+import dev.ragnarok.fenrir.settings.theme.ThemesController.currentStyle
+import dev.ragnarok.fenrir.util.Logger
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.toast.CustomToast.Companion.createCustomToast
+import java.io.UnsupportedEncodingException
+import java.util.regex.Pattern
+
+class LoginActivity : AppCompatActivity() {
+ private var TLogin: String? = null
+ private var TPassword: String? = null
+ private var TwoFA: String? = null
+ private var isSave = false
+
+ @SuppressLint("SetJavaScriptEnabled")
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ setTheme(currentStyle())
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_login)
+ val webview = findViewById(R.id.vkontakteview)
+ webview.settings.javaScriptEnabled = true
+ webview.clearCache(true)
+ webview.settings.userAgentString = USER_AGENT(Constants.DEFAULT_ACCOUNT_TYPE)
+
+ //Чтобы получать уведомления об окончании загрузки страницы
+ webview.webViewClient = VkontakteWebViewClient()
+ val cookieManager = CookieManager.getInstance()
+ cookieManager.removeAllCookies { aBoolean: Boolean ->
+ Log.d(
+ TAG,
+ "Cookie removed: $aBoolean"
+ )
+ }
+ if (intent.getStringExtra(EXTRA_VALIDATE) == null) {
+ val clientId = intent.getStringExtra(EXTRA_CLIENT_ID) ?: return
+ val scope = intent.getStringExtra(EXTRA_SCOPE) ?: return
+ val groupIds = intent.getStringExtra(EXTRA_GROUP_IDS) ?: return
+ try {
+ if (groupIds.nonNullNoEmpty()) {
+ webview.settings.userAgentString = Constants.KATE_USER_AGENT
+ }
+ val url = Auth.getUrl(clientId, scope, groupIds)
+ webview.loadUrl(url)
+ } catch (e: UnsupportedEncodingException) {
+ createCustomToast(this).showToastError(e.localizedMessage)
+ }
+ } else {
+ TLogin = intent.getStringExtra(EXTRA_LOGIN)
+ TPassword = intent.getStringExtra(EXTRA_PASSWORD)
+ TwoFA = intent.getStringExtra(EXTRA_TWO_FA)
+ isSave = intent.getBooleanExtra(EXTRA_SAVE, false)
+ webview.loadUrl(intent.getStringExtra(EXTRA_VALIDATE) ?: return)
+ }
+ }
+
+ internal fun parseUrl(url: String?) {
+ try {
+ if (url == null) {
+ return
+ }
+ Logger.d(TAG, "url=$url")
+ if (url.startsWith(Auth.redirect_url)) {
+ if (!url.contains("error=")) {
+ val intent = Intent()
+ try {
+ val tokens = tryExtractAccessTokens(url)
+ intent.putParcelableArrayListExtra("group_tokens", tokens)
+ } catch (e: Exception) {
+ val accessToken = tryExtractAccessToken(url)
+ val userId = tryExtractUserId(url)
+ intent.putExtra(Extra.TOKEN, accessToken)
+ intent.putExtra(Extra.USER_ID, userId?.toInt())
+ intent.putExtra(Extra.LOGIN, TLogin)
+ intent.putExtra(Extra.PASSWORD, TPassword)
+ intent.putExtra(Extra.TWO_FA, TwoFA)
+ intent.putExtra(Extra.SAVE, isSave)
+ }
+ setResult(RESULT_OK, intent)
+ }
+ finish()
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ private inner class VkontakteWebViewClient : WebViewClient() {
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ parseUrl(url)
+ }
+ }
+
+ companion object {
+ private val TAG = LoginActivity::class.java.simpleName
+ private const val EXTRA_CLIENT_ID = "client_id"
+ private const val EXTRA_SCOPE = "scope"
+ private const val EXTRA_VALIDATE = "validate"
+ private const val EXTRA_LOGIN = "login"
+ private const val EXTRA_PASSWORD = "password"
+ private const val EXTRA_TWO_FA = "two_fa"
+ private const val EXTRA_SAVE = "save"
+ private const val EXTRA_GROUP_IDS = "group_ids"
+
+
+ fun createIntent(context: Context?, clientId: String?, scope: String?): Intent {
+ return Intent(context, LoginActivity::class.java)
+ .putExtra(EXTRA_CLIENT_ID, clientId)
+ .putExtra(EXTRA_SCOPE, scope)
+ }
+
+
+ fun createIntent(
+ context: Context?,
+ validate_url: String?,
+ Login: String?,
+ Password: String?,
+ TwoFa: String?,
+ isSave: Boolean
+ ): Intent {
+ return Intent(context, LoginActivity::class.java)
+ .putExtra(EXTRA_VALIDATE, validate_url).putExtra(EXTRA_LOGIN, Login)
+ .putExtra(EXTRA_PASSWORD, Password).putExtra(EXTRA_TWO_FA, TwoFa)
+ .putExtra(EXTRA_SAVE, isSave)
+ }
+
+
+ fun createIntent(
+ context: Context?,
+ clientId: String?,
+ scope: String?,
+ groupIds: Collection?
+ ): Intent {
+ val ids = Utils.join(groupIds, ",", object : Utils.SimpleFunction {
+ override fun apply(orig: Int): String {
+ return orig.toString()
+ }
+ })
+ return Intent(context, LoginActivity::class.java)
+ .putExtra(EXTRA_CLIENT_ID, clientId)
+ .putExtra(EXTRA_SCOPE, scope)
+ .putExtra(EXTRA_GROUP_IDS, ids)
+ }
+
+ internal fun tryExtractAccessToken(url: String): String? {
+ return VKStringUtils.extractPattern(url, "access_token=(.*?)&")
+ }
+
+ @Throws(Exception::class)
+ internal fun tryExtractAccessTokens(url: String): ArrayList {
+ val p = Pattern.compile("access_token_(\\d*)=(.*?)(&|$)")
+ val tokens = ArrayList()
+ val matcher = p.matcher(url)
+ while (matcher.find()) {
+ val groupid = matcher.group(1)
+ val token = matcher.group(2)
+ if (groupid.nonNullNoEmpty() && token.nonNullNoEmpty()) {
+ tokens.add(Token(-groupid.toInt(), token))
+ }
+ }
+ if (tokens.isEmpty()) {
+ throw Exception("Failed to parse redirect url $url")
+ }
+ return tokens
+ }
+
+ internal fun tryExtractUserId(url: String): String? {
+ return VKStringUtils.extractPattern(url, "user_id=(\\d*)")
+ }
+
+
+ fun extractGroupTokens(data: Intent): ArrayList? {
+ return data.getParcelableArrayListExtraCompat("group_tokens")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/LottieActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/LottieActivity.kt
new file mode 100644
index 000000000..44efddd9d
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/LottieActivity.kt
@@ -0,0 +1,175 @@
+package dev.ragnarok.fenrir.activity
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.os.Environment
+import android.view.WindowManager
+import android.widget.TextView
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.documentfile.provider.DocumentFile
+import com.google.android.material.button.MaterialButton
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.module.BufferWriteNative
+import dev.ragnarok.fenrir.module.FenrirNative
+import dev.ragnarok.fenrir.module.rlottie.RLottie2Gif
+import dev.ragnarok.fenrir.module.rlottie.RLottie2Gif.Lottie2GifListener
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.ISettings
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.settings.theme.ThemesController.currentStyle
+import dev.ragnarok.fenrir.util.AppPerms
+import dev.ragnarok.fenrir.util.AppPerms.requestPermissionsAbs
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.view.natives.rlottie.RLottieImageView
+import java.io.File
+
+class LottieActivity : AppCompatActivity() {
+ private val requestWritePermission = requestPermissionsAbs(
+ arrayOf(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE
+ )
+ ) {
+ startExportGif()
+ }
+ private var toGif: MaterialButton? = null
+
+ private val fManager = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result: ActivityResult ->
+ if (result.resultCode == RESULT_OK && result.data != null) {
+ val v = DocumentFile.fromSingleUri(
+ this,
+ intent.data ?: return@registerForActivityResult
+ )
+ val title: String = if (v == null || v.name.isNullOrEmpty()) {
+ "converted.gif"
+ } else {
+ v.name + ".gif"
+ }
+ val file = File(
+ result.data?.getStringExtra(Extra.PATH), title
+ )
+ toGif?.isEnabled = false
+ RLottie2Gif.create(
+ BufferWriteNative.fromStreamEndlessNull(
+ contentResolver.openInputStream(
+ intent.data ?: return@registerForActivityResult
+ ) ?: return@registerForActivityResult
+ )
+ ).setListener(object : Lottie2GifListener {
+ var start: Long = 0
+ var logs: String? = null
+ override fun onStarted() {
+ start = System.currentTimeMillis()
+ logs = "Wait a moment...\r\n"
+ log(logs)
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun onProgress(frame: Int, totalFrame: Int) {
+ log(logs + "progress : " + frame + "/" + totalFrame)
+ }
+
+ override fun onFinished() {
+ logs =
+ "GIF created (" + (System.currentTimeMillis() - start) + "ms)\r\n" +
+ "Resolution : " + 500 + "x" + 500 + "\r\n" +
+ "Path : " + file + "\r\n" +
+ "File Size : " + file.length() / 1024 + "kb"
+ log(logs)
+ toGif?.post { toGif?.isEnabled = true }
+ }
+ })
+ .setBackgroundColor(Color.TRANSPARENT)
+ .setOutputPath(file)
+ .setSize(500, 500)
+ .setBackgroundTask(true)
+ .setDithering(false)
+ .build()
+ }
+ }
+
+ private var lottie: RLottieImageView? = null
+ private var lg: TextView? = null
+ internal fun log(log: String?) {
+ lg?.post { lg?.text = log?.trim { it <= ' ' } }
+ }
+
+ override fun attachBaseContext(newBase: Context) {
+ super.attachBaseContext(Utils.updateActivityContext(newBase))
+ }
+
+ @Suppress("DEPRECATION")
+ private fun startExportGif() {
+ fManager.launch(
+ FileManagerSelectActivity.makeFileManager(
+ this, Environment.getExternalStorageDirectory().absolutePath,
+ "dirs"
+ )
+ )
+ }
+
+ @Suppress("DEPRECATION")
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ setTheme(currentStyle())
+ Utils.prepareDensity(this)
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_lottie)
+ lottie = findViewById(R.id.lottie_preview)
+ lg = findViewById(R.id.log_tag)
+ toGif = findViewById(R.id.lottie_to_gif)
+ toGif?.setOnClickListener {
+ if (!FenrirNative.isNativeLoaded) {
+ return@setOnClickListener
+ }
+ if (!AppPerms.hasReadWriteStoragePermission(this)) {
+ requestWritePermission.launch()
+ return@setOnClickListener
+ }
+ startExportGif()
+ }
+ val w = window
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.statusBarColor = CurrentTheme.getStatusBarColor(this)
+ w.navigationBarColor = CurrentTheme.getNavigationBarColor(this)
+ if (savedInstanceState == null) {
+ handleIntent(intent)
+ }
+ }
+
+ private fun handleIntent(intent: Intent?) {
+ if (intent == null) {
+ finish()
+ return
+ }
+ val accountId = Settings.get().accounts().current
+ if (accountId == ISettings.IAccountsSettings.INVALID_ID) {
+ finish()
+ return
+ }
+ val action = intent.action
+ if (Intent.ACTION_VIEW == action) {
+ try {
+ lottie?.setAutoRepeat(true)
+ lottie?.fromString(
+ BufferWriteNative.fromStreamEndlessNull(
+ contentResolver.openInputStream(
+ getIntent().data ?: return
+ ) ?: return
+ ), Utils.dp(500f), Utils.dp(500f)
+ )
+ lottie?.playAnimation()
+ } catch (e: Throwable) {
+ e.printStackTrace()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/MainActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/MainActivity.kt
new file mode 100644
index 000000000..0fb1098c5
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/MainActivity.kt
@@ -0,0 +1,1722 @@
+package dev.ragnarok.fenrir.activity
+
+import android.animation.ObjectAnimator
+import android.animation.PropertyValuesHolder
+import android.app.Activity
+import android.content.*
+import android.graphics.Color
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.IBinder
+import android.view.*
+import android.view.animation.LinearInterpolator
+import android.view.inputmethod.InputMethodManager
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.ColorInt
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import androidx.drawerlayout.widget.DrawerLayout
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentContainerView
+import androidx.fragment.app.FragmentManager
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
+import com.google.android.material.bottomnavigation.BottomNavigationView
+import com.google.android.material.card.MaterialCardView
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.navigation.NavigationBarView
+import com.google.android.material.snackbar.BaseTransientBottomBar
+import dev.ragnarok.fenrir.*
+import dev.ragnarok.fenrir.Includes.networkInterfaces
+import dev.ragnarok.fenrir.Includes.provideMainThreadScheduler
+import dev.ragnarok.fenrir.Includes.proxySettings
+import dev.ragnarok.fenrir.Includes.pushRegistrationResolver
+import dev.ragnarok.fenrir.activity.ActivityUtils.checkInputExist
+import dev.ragnarok.fenrir.activity.ActivityUtils.isMimeAudio
+import dev.ragnarok.fenrir.activity.EnterPinActivity.Companion.getClass
+import dev.ragnarok.fenrir.activity.gifpager.GifPagerActivity
+import dev.ragnarok.fenrir.activity.photopager.PhotoPagerActivity.Companion.newInstance
+import dev.ragnarok.fenrir.activity.qr.CameraScanActivity
+import dev.ragnarok.fenrir.activity.storypager.StoryPagerActivity
+import dev.ragnarok.fenrir.db.Stores
+import dev.ragnarok.fenrir.dialog.ResolveDomainDialog
+import dev.ragnarok.fenrir.domain.InteractorFactory
+import dev.ragnarok.fenrir.domain.impl.CountersInteractor
+import dev.ragnarok.fenrir.fragment.*
+import dev.ragnarok.fenrir.fragment.PreferencesFragment.Companion.cleanCache
+import dev.ragnarok.fenrir.fragment.abswall.AbsWallFragment
+import dev.ragnarok.fenrir.fragment.attachments.commentcreate.CommentCreateFragment
+import dev.ragnarok.fenrir.fragment.attachments.commentedit.CommentEditFragment
+import dev.ragnarok.fenrir.fragment.attachments.postcreate.PostCreateFragment
+import dev.ragnarok.fenrir.fragment.attachments.postedit.PostEditFragment
+import dev.ragnarok.fenrir.fragment.attachments.repost.RepostFragment
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment
+import dev.ragnarok.fenrir.fragment.audio.AudiosTabsFragment
+import dev.ragnarok.fenrir.fragment.audio.audios.AudiosFragment
+import dev.ragnarok.fenrir.fragment.audio.audiosbyartist.AudiosByArtistFragment
+import dev.ragnarok.fenrir.fragment.audio.audiosrecommendation.AudiosRecommendationFragment
+import dev.ragnarok.fenrir.fragment.audio.catalog_v2.listedit.CatalogV2ListEditFragment
+import dev.ragnarok.fenrir.fragment.audio.catalog_v2.lists.CatalogV2ListFragment
+import dev.ragnarok.fenrir.fragment.audio.catalog_v2.sections.CatalogV2SectionFragment
+import dev.ragnarok.fenrir.fragment.comments.CommentsFragment
+import dev.ragnarok.fenrir.fragment.communities.CommunitiesFragment
+import dev.ragnarok.fenrir.fragment.communitycontrol.CommunityControlFragment
+import dev.ragnarok.fenrir.fragment.communitycontrol.communityban.CommunityBanEditFragment
+import dev.ragnarok.fenrir.fragment.communitycontrol.communityinfocontacts.CommunityInfoContactsFragment
+import dev.ragnarok.fenrir.fragment.communitycontrol.communityinfolinks.CommunityInfoLinksFragment
+import dev.ragnarok.fenrir.fragment.communitycontrol.communitymanageredit.CommunityManagerEditFragment
+import dev.ragnarok.fenrir.fragment.communitycontrol.communitymembers.CommunityMembersFragment
+import dev.ragnarok.fenrir.fragment.conversation.ConversationFragmentFactory
+import dev.ragnarok.fenrir.fragment.createphotoalbum.CreatePhotoAlbumFragment
+import dev.ragnarok.fenrir.fragment.createpoll.CreatePollFragment
+import dev.ragnarok.fenrir.fragment.docs.DocsFragment
+import dev.ragnarok.fenrir.fragment.docs.DocsListPresenter
+import dev.ragnarok.fenrir.fragment.fave.FaveTabsFragment
+import dev.ragnarok.fenrir.fragment.feed.FeedFragment
+import dev.ragnarok.fenrir.fragment.feedback.FeedbackFragment
+import dev.ragnarok.fenrir.fragment.feedbackvkofficial.FeedbackVKOfficialFragment
+import dev.ragnarok.fenrir.fragment.feedbanned.FeedBannedFragment
+import dev.ragnarok.fenrir.fragment.friends.birthday.BirthDayFragment
+import dev.ragnarok.fenrir.fragment.friends.friendsbyphones.FriendsByPhonesFragment
+import dev.ragnarok.fenrir.fragment.friends.friendstabs.FriendsTabsFragment
+import dev.ragnarok.fenrir.fragment.gifts.GiftsFragment
+import dev.ragnarok.fenrir.fragment.groupchats.GroupChatsFragment
+import dev.ragnarok.fenrir.fragment.likes.LikesFragment
+import dev.ragnarok.fenrir.fragment.localserver.filemanagerremote.FileManagerRemoteFragment
+import dev.ragnarok.fenrir.fragment.localserver.photoslocalserver.PhotosLocalServerFragment
+import dev.ragnarok.fenrir.fragment.logs.LogsFragment
+import dev.ragnarok.fenrir.fragment.marketview.MarketViewFragment
+import dev.ragnarok.fenrir.fragment.messages.chat.ChatFragment
+import dev.ragnarok.fenrir.fragment.messages.chat.ChatFragment.Companion.newInstance
+import dev.ragnarok.fenrir.fragment.messages.chatmembers.ChatMembersFragment
+import dev.ragnarok.fenrir.fragment.messages.dialogs.DialogsFragment
+import dev.ragnarok.fenrir.fragment.messages.fwds.FwdsFragment
+import dev.ragnarok.fenrir.fragment.messages.importantmessages.ImportantMessagesFragment
+import dev.ragnarok.fenrir.fragment.messages.messageslook.MessagesLookFragment
+import dev.ragnarok.fenrir.fragment.messages.notreadmessages.NotReadMessagesFragment
+import dev.ragnarok.fenrir.fragment.narratives.NarrativesFragment
+import dev.ragnarok.fenrir.fragment.navigationedit.DrawerEditFragment
+import dev.ragnarok.fenrir.fragment.navigationedit.SideDrawerEditFragment
+import dev.ragnarok.fenrir.fragment.newsfeedcomments.NewsfeedCommentsFragment
+import dev.ragnarok.fenrir.fragment.newsfeedmentions.NewsfeedMentionsFragment
+import dev.ragnarok.fenrir.fragment.ownerarticles.OwnerArticlesFragment
+import dev.ragnarok.fenrir.fragment.photoallcomment.PhotoAllCommentFragment
+import dev.ragnarok.fenrir.fragment.poll.PollFragment
+import dev.ragnarok.fenrir.fragment.productalbums.ProductAlbumsFragment
+import dev.ragnarok.fenrir.fragment.products.ProductsFragment
+import dev.ragnarok.fenrir.fragment.requestexecute.RequestExecuteFragment
+import dev.ragnarok.fenrir.fragment.search.AudioSearchTabsFragment
+import dev.ragnarok.fenrir.fragment.search.SearchTabsFragment
+import dev.ragnarok.fenrir.fragment.search.SingleTabSearchFragment
+import dev.ragnarok.fenrir.fragment.shortcutsview.ShortcutsViewFragment
+import dev.ragnarok.fenrir.fragment.shortedlinks.ShortedLinksFragment
+import dev.ragnarok.fenrir.fragment.theme.ThemeFragment
+import dev.ragnarok.fenrir.fragment.topics.TopicsFragment
+import dev.ragnarok.fenrir.fragment.userbanned.UserBannedFragment
+import dev.ragnarok.fenrir.fragment.userdetails.UserDetailsFragment.Companion.newInstance
+import dev.ragnarok.fenrir.fragment.videoalbumsbyvideo.VideoAlbumsByVideoFragment
+import dev.ragnarok.fenrir.fragment.videopreview.VideoPreviewFragment
+import dev.ragnarok.fenrir.fragment.videos.IVideosListView
+import dev.ragnarok.fenrir.fragment.videos.VideosFragment
+import dev.ragnarok.fenrir.fragment.videos.VideosTabsFragment
+import dev.ragnarok.fenrir.fragment.vkphotoalbums.VKPhotoAlbumsFragment
+import dev.ragnarok.fenrir.fragment.vkphotos.IVkPhotosView
+import dev.ragnarok.fenrir.fragment.vkphotos.VKPhotosFragment
+import dev.ragnarok.fenrir.fragment.voters.VotersFragment
+import dev.ragnarok.fenrir.fragment.wallattachments.WallAttachmentsFragmentFactory
+import dev.ragnarok.fenrir.fragment.wallattachments.wallsearchcommentsattachments.WallSearchCommentsAttachmentsFragment
+import dev.ragnarok.fenrir.fragment.wallpost.WallPostFragment
+import dev.ragnarok.fenrir.link.LinkHelper
+import dev.ragnarok.fenrir.listener.*
+import dev.ragnarok.fenrir.media.music.MusicPlaybackController
+import dev.ragnarok.fenrir.media.music.MusicPlaybackController.ServiceToken
+import dev.ragnarok.fenrir.media.music.MusicPlaybackController.bindToServiceWithoutStart
+import dev.ragnarok.fenrir.media.music.MusicPlaybackController.isPlaying
+import dev.ragnarok.fenrir.media.music.MusicPlaybackController.stop
+import dev.ragnarok.fenrir.media.music.MusicPlaybackController.unbindFromService
+import dev.ragnarok.fenrir.media.music.MusicPlaybackService
+import dev.ragnarok.fenrir.media.music.MusicPlaybackService.Companion.startForPlayList
+import dev.ragnarok.fenrir.modalbottomsheetdialogfragment.ModalBottomSheetDialogFragment
+import dev.ragnarok.fenrir.modalbottomsheetdialogfragment.Option
+import dev.ragnarok.fenrir.modalbottomsheetdialogfragment.OptionRequest
+import dev.ragnarok.fenrir.model.*
+import dev.ragnarok.fenrir.model.drawer.AbsMenuItem
+import dev.ragnarok.fenrir.model.drawer.RecentChat
+import dev.ragnarok.fenrir.model.drawer.SectionMenuItem
+import dev.ragnarok.fenrir.module.FenrirNative
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceFactory
+import dev.ragnarok.fenrir.place.PlaceProvider
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.ISettings
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.settings.SwipesChatMode
+import dev.ragnarok.fenrir.settings.theme.ThemesController.currentStyle
+import dev.ragnarok.fenrir.settings.theme.ThemesController.nextRandom
+import dev.ragnarok.fenrir.upload.UploadUtils
+import dev.ragnarok.fenrir.util.*
+import dev.ragnarok.fenrir.util.HelperSimple.needHelp
+import dev.ragnarok.fenrir.util.Pair.Companion.create
+import dev.ragnarok.fenrir.util.rxutils.RxUtils
+import dev.ragnarok.fenrir.util.toast.CustomSnackbars
+import dev.ragnarok.fenrir.util.toast.CustomToast.Companion.createCustomToast
+import dev.ragnarok.fenrir.view.navigation.AbsNavigationView
+import dev.ragnarok.fenrir.view.navigation.AbsNavigationView.NavigationDrawerCallbacks
+import dev.ragnarok.fenrir.view.zoomhelper.ZoomHelper.Companion.getInstance
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+import java.util.concurrent.TimeUnit
+
+open class MainActivity : AppCompatActivity(), NavigationDrawerCallbacks, OnSectionResumeCallback,
+ AppStyleable, PlaceProvider, ServiceConnection, UpdatableNavigation,
+ NavigationBarView.OnItemSelectedListener {
+ private val mCompositeDisposable = CompositeDisposable()
+ private val postResumeActions: MutableList> = ArrayList(0)
+ private val requestEnterPin = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result: ActivityResult ->
+ if (result.resultCode != RESULT_OK) {
+ finish()
+ }
+ }
+ protected var mAccountId = 0
+ private val requestEnterPinZero = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result: ActivityResult ->
+ if (result.resultCode != RESULT_OK) {
+ finish()
+ } else {
+ Settings.get().ui().getDefaultPage(mAccountId).tryOpenWith(this)
+ }
+ }
+ private val requestQRScan = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result: ActivityResult ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val scanner = result.data?.extras?.getString(Extra.URL)
+ if (scanner.nonNullNoEmpty()) {
+ MaterialAlertDialogBuilder(this)
+ .setIcon(R.drawable.qr_code)
+ .setMessage(scanner)
+ .setTitle(getString(R.string.scan_qr))
+ .setPositiveButton(R.string.open) { _: DialogInterface?, _: Int ->
+ LinkHelper.openUrl(
+ this,
+ mAccountId,
+ scanner, false
+ )
+ }
+ .setNeutralButton(R.string.copy_text) { _: DialogInterface?, _: Int ->
+ val clipboard = getSystemService(
+ CLIPBOARD_SERVICE
+ ) as ClipboardManager?
+ val clip = ClipData.newPlainText("response", scanner)
+ clipboard?.setPrimaryClip(clip)
+ createCustomToast(this).showToast(R.string.copied_to_clipboard)
+ }
+ .setCancelable(true)
+ .create().show()
+ }
+ }
+ }
+ private val requestLogin = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ mAccountId = Settings.get()
+ .accounts()
+ .current
+ if (mAccountId == ISettings.IAccountsSettings.INVALID_ID) {
+ supportFinishAfterTransition()
+ }
+ }
+ protected var mLayoutRes = if (Settings.get().main().isSnow_mode) snowLayout else normalLayout
+ protected var mLastBackPressedTime: Long = 0
+
+ /**
+ * Атрибуты секции, которая на данный момент находится на главном контейнере экрана
+ */
+ private var mCurrentFrontSection: AbsMenuItem? = null
+ private var mToolbar: Toolbar? = null
+ private var mBottomNavigation: BottomNavigationView? = null
+ private var mBottomNavigationContainer: ViewGroup? = null
+ private var mViewFragment: FragmentContainerView? = null
+ private val requestLoginZero = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ mAccountId = Settings.get()
+ .accounts()
+ .current
+ if (mAccountId == ISettings.IAccountsSettings.INVALID_ID) {
+ supportFinishAfterTransition()
+ } else {
+ Settings.get().ui().getDefaultPage(mAccountId).tryOpenWith(this)
+ checkFCMRegistration(true)
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP && needHelp(
+ HelperSimple.LOLLIPOP_21,
+ 1
+ )
+ ) {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.info)
+ .setMessage(R.string.lollipop21)
+ .setCancelable(false)
+ .setPositiveButton(R.string.button_ok, null)
+ .show()
+ }
+ }
+ }
+ private val mOnBackStackChangedListener = FragmentManager.OnBackStackChangedListener {
+ resolveToolbarNavigationIcon()
+ keyboardHide()
+ }
+ private var mAudioPlayServiceToken: ServiceToken? = null
+ private var isActivityDestroyed = false
+
+ /**
+ * First - DrawerItem, second - Clear back stack before adding
+ */
+ private var mTargetPage: Pair? = null
+ private var resumed = false
+ private var isZoomPhoto = false
+ private val snowLayout: Int
+ get() = if (Settings.get()
+ .other().is_side_navigation()
+ ) R.layout.activity_main_side_with_snow else R.layout.activity_main_with_snow
+ private val normalLayout: Int
+ get() = if (Settings.get()
+ .other().is_side_navigation()
+ ) R.layout.activity_main_side else R.layout.activity_main
+
+ @MainActivityTransforms
+ protected open fun getMainActivityTransform(): Int {
+ return MainActivityTransforms.MAIN
+ }
+
+ private fun postResume(action: Action) {
+ if (resumed) {
+ action.call(this)
+ } else {
+ postResumeActions.add(action)
+ }
+ }
+
+ override fun attachBaseContext(newBase: Context) {
+ super.attachBaseContext(Utils.updateActivityContext(newBase))
+ }
+
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+ if (navigationView?.checkCloseByClick(ev) == true) {
+ return true
+ }
+ return if (!isZoomPhoto) {
+ super.dispatchTouchEvent(ev)
+ } else getInstance()?.dispatchTouchEvent(ev, this) == true || super.dispatchTouchEvent(ev)
+ }
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ delegate.applyDayNight()
+ if (savedInstanceState == null && getMainActivityTransform() == MainActivityTransforms.MAIN) {
+ nextRandom()
+ }
+ setTheme(currentStyle())
+ Utils.prepareDensity(this)
+ super.onCreate(savedInstanceState)
+ isActivityDestroyed = false
+ isZoomPhoto = Settings.get().other().isDo_zoom_photo
+ mCompositeDisposable.add(
+ Settings.get()
+ .accounts()
+ .observeChanges()
+ .observeOn(provideMainThreadScheduler())
+ .subscribe { onCurrentAccountChange(it) })
+ mCompositeDisposable.add(
+ proxySettings
+ .observeActive().observeOn(provideMainThreadScheduler())
+ .subscribe { stop() })
+ mCompositeDisposable.add(Stores.instance
+ .dialogs()
+ .observeUnreadDialogsCount()
+ .filter { it.first == mAccountId }
+ .toMainThread()
+ .subscribe { updateMessagesBagde(it.second) })
+ bindToAudioPlayService()
+ setContentView(mLayoutRes)
+ mAccountId = Settings.get()
+ .accounts()
+ .current
+ setStatusbarColored(true, Settings.get().ui().isDarkModeEnabled(this))
+ val mDrawerLayout = findViewById(R.id.my_drawer_layout)
+
+ mViewFragment = findViewById(R.id.fragment)
+ val anim: ObjectAnimator
+ if (mDrawerLayout != null && Settings.get().other().is_side_navigation()) {
+ navigationView?.setUp(mDrawerLayout)
+ anim = ObjectAnimator.ofPropertyValuesHolder(
+ mViewFragment, PropertyValuesHolder.ofFloat(
+ View.ALPHA, 1f, 1f, 0.6f
+ ),
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0f, 0f, 100f)
+ )
+ anim.interpolator = LinearInterpolator()
+ } else {
+ anim = ObjectAnimator.ofPropertyValuesHolder(
+ mViewFragment, PropertyValuesHolder.ofFloat(
+ View.ALPHA, 1f, 1f, 0.6f
+ ),
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f, 0f, -100f)
+ )
+ anim.interpolator = LinearInterpolator()
+ }
+ navigationView?.setStatesCallback(object : AbsNavigationView.NavigationStatesCallbacks {
+ override fun onMove(slideOffset: Float) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+ anim.currentPlayTime =
+ (slideOffset * anim.duration).toLong().coerceAtMost(anim.duration)
+ } else {
+ anim.setCurrentFraction(slideOffset)
+ }
+ }
+
+ override fun onOpened() {
+ anim.start()
+ }
+
+ override fun onClosed() {
+ anim.cancel()
+ }
+
+ override fun closeKeyboard() {
+ keyboardHide()
+ }
+
+ })
+ mBottomNavigation = findViewById(R.id.bottom_navigation_menu)
+ mBottomNavigation?.setOnItemSelectedListener(this)
+ mBottomNavigationContainer = findViewById(R.id.bottom_navigation_menu_container)
+ supportFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener)
+ resolveToolbarNavigationIcon()
+ updateMessagesBagde(
+ Stores.instance
+ .dialogs()
+ .getUnreadDialogsCount(mAccountId)
+ )
+ if (savedInstanceState == null) {
+ val intentWasHandled = handleIntent(intent, true)
+ if (!isAuthValid) {
+ if (intentWasHandled) {
+ startAccountsActivity()
+ } else {
+ startAccountsActivityZero()
+ }
+ } else {
+ if (getMainActivityTransform() == MainActivityTransforms.MAIN) {
+ checkFCMRegistration(false)
+ mCompositeDisposable.add(MusicPlaybackController.tracksExist.findAllAudios(this)
+ .andThen(
+ InteractorFactory.createStickersInteractor().placeToStickerCache(this)
+ )
+ .fromIOToMain()
+ .subscribe(RxUtils.dummy()) { t ->
+ if (Settings.get().other().isDeveloper_mode) {
+ createCustomToast(this).showToastThrowable(t)
+ }
+ })
+ Settings.get().other().get_last_audio_sync().let {
+ if (it > 0 && (System.currentTimeMillis() / 1000L) - it > 900) {
+ Settings.get().other().set_last_audio_sync(-1)
+ mCompositeDisposable.add(
+ Includes.stores.tempStore().deleteAudios()
+ .fromIOToMain()
+ .subscribe(RxUtils.dummy(), RxUtils.ignore())
+ )
+ }
+ }
+ Settings.get().other().appStoredVersionEqual()
+ if (Settings.get().other().isDelete_cache_images) {
+ cleanCache(this, false)
+ }
+ }
+ updateNotificationCount(mAccountId)
+ val needPin = (Settings.get().security().isUsePinForEntrance
+ && !intent.getBooleanExtra(EXTRA_NO_REQUIRE_PIN, false) && !Settings.get()
+ .security().isDelayedAllow)
+ if (needPin) {
+ if (!intentWasHandled) {
+ startEnterPinActivityZero()
+ } else {
+ startEnterPinActivity()
+ }
+ } else {
+ if (!intentWasHandled) {
+ Settings.get().ui().getDefaultPage(mAccountId).tryOpenWith(this)
+ }
+ }
+ }
+ }
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (navigationView?.isSheetOpen == true) {
+ navigationView?.closeSheet()
+ return
+ }
+ val front = frontFragment
+ if (front is BackPressCallback) {
+ if (!(front as BackPressCallback).onBackPressed()) {
+ return
+ }
+ }
+ if (supportFragmentManager.backStackEntryCount == 1) {
+ if (getMainActivityTransform() != MainActivityTransforms.SWIPEBLE) {
+ if (isFragmentWithoutNavigation) {
+ openNavigationPage(AbsNavigationView.SECTION_ITEM_FEED, false)
+ return
+ }
+ if (isChatFragment) {
+ openNavigationPage(AbsNavigationView.SECTION_ITEM_DIALOGS, false)
+ return
+ }
+ }
+ if (mLastBackPressedTime < 0
+ || mLastBackPressedTime + DOUBLE_BACK_PRESSED_TIMEOUT > System.currentTimeMillis()
+ ) {
+ supportFinishAfterTransition()
+ return
+ }
+ mLastBackPressedTime = System.currentTimeMillis()
+ CustomSnackbars.createCustomSnackbars(mViewFragment, mBottomNavigationContainer)
+ ?.setDurationSnack(BaseTransientBottomBar.LENGTH_SHORT)
+ ?.defaultSnack(R.string.click_back_to_exit)?.show()
+ } else {
+ supportFragmentManager.popBackStack()
+ }
+ }
+ })
+ //CurrentTheme.dumpDynamicColors(this)
+ }
+
+ private fun updateNotificationCount(account: Int) {
+ mCompositeDisposable.add(CountersInteractor(networkInterfaces).getApiCounters(account)
+ .fromIOToMain()
+ .subscribe({ counters -> updateNotificationsBadge(counters) }) { removeNotificationsBadge() })
+ }
+
+ override fun onPause() {
+ resumed = false
+ super.onPause()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ resumed = true
+ for (action in postResumeActions) {
+ action.call(this)
+ }
+ postResumeActions.clear()
+ }
+
+ private fun startEnterPinActivity() {
+ val intent = Intent(this, getClass(this))
+ requestEnterPin.launch(intent)
+ }
+
+ private fun startEnterPinActivityZero() {
+ val intent = Intent(this, getClass(this))
+ requestEnterPinZero.launch(intent)
+ }
+
+ private fun checkFCMRegistration(onlyCheckGMS: Boolean) {
+ if (!checkPlayServices(this)) {
+ if (!Settings.get().other().isDisabledErrorFCM) {
+ mViewFragment?.let {
+ CustomSnackbars.createCustomSnackbars(mViewFragment, mBottomNavigationContainer)
+ ?.setDurationSnack(BaseTransientBottomBar.LENGTH_LONG)
+ ?.themedSnack(R.string.this_device_does_not_support_fcm)
+ ?.setAction(R.string.button_access) {
+ Settings.get().other().setDisableErrorFCM(true)
+ }
+ ?.show()
+ }
+ }
+ return
+ }
+ if (onlyCheckGMS) {
+ return
+ }
+ mCompositeDisposable.add(
+ pushRegistrationResolver.resolvePushRegistration()
+ .fromIOToMain()
+ .subscribe(RxUtils.dummy(), RxUtils.ignore())
+ )
+
+ //RequestHelper.checkPushRegistration(this);
+ }
+
+ private fun bindToAudioPlayService() {
+ if (!isActivityDestroyed && mAudioPlayServiceToken == null) {
+ mAudioPlayServiceToken = bindToServiceWithoutStart(this, this)
+ }
+ }
+
+ private fun resolveToolbarNavigationIcon() {
+ mToolbar ?: return
+ val manager = supportFragmentManager
+ if (manager.backStackEntryCount > 1 || frontFragment is CanBackPressedCallback && (frontFragment as CanBackPressedCallback?)?.canBackPressed() == true) {
+ mToolbar?.setNavigationIcon(R.drawable.arrow_left)
+ mToolbar?.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
+ } else {
+ if (!isFragmentWithoutNavigation) {
+ mToolbar?.setNavigationIcon(R.drawable.client_round)
+ mToolbar?.setNavigationOnClickListener {
+ val menus = ModalBottomSheetDialogFragment.Builder()
+ menus.add(
+ OptionRequest(
+ R.id.button_ok,
+ getString(R.string.set_offline),
+ R.drawable.offline,
+ true
+ )
+ )
+ menus.add(
+ OptionRequest(
+ R.id.button_cancel,
+ getString(R.string.open_clipboard_url),
+ R.drawable.web,
+ false
+ )
+ )
+ menus.add(
+ OptionRequest(
+ R.id.action_preferences,
+ getString(R.string.settings),
+ R.drawable.preferences,
+ true
+ )
+ )
+ menus.add(
+ OptionRequest(
+ R.id.button_camera,
+ getString(R.string.scan_qr),
+ R.drawable.qr_code,
+ false
+ )
+ )
+ menus.show(
+ supportFragmentManager,
+ "left_options",
+ object : ModalBottomSheetDialogFragment.Listener {
+ override fun onModalOptionSelected(option: Option) {
+ when {
+ option.id == R.id.button_ok -> {
+ mCompositeDisposable.add(InteractorFactory.createAccountInteractor()
+ .setOffline(
+ Settings.get().accounts().current
+ )
+ .fromIOToMain()
+ .subscribe({ onSetOffline(it) }) {
+ onSetOffline(
+ false
+ )
+ })
+ }
+ option.id == R.id.button_cancel -> {
+ val clipBoard =
+ getSystemService(CLIPBOARD_SERVICE) as ClipboardManager?
+ if (clipBoard != null && clipBoard.primaryClip != null && (clipBoard.primaryClip?.itemCount
+ ?: 0) > 0 && (clipBoard.primaryClip
+ ?: return).getItemAt(0).text != null
+ ) {
+ val temp =
+ clipBoard.primaryClip?.getItemAt(0)?.text.toString()
+ LinkHelper.openUrl(
+ this@MainActivity,
+ mAccountId,
+ temp,
+ false
+ )
+ }
+ }
+ option.id == R.id.action_preferences -> {
+ PlaceFactory.getPreferencesPlace(mAccountId)
+ .tryOpenWith(this@MainActivity)
+ }
+ option.id == R.id.button_camera && FenrirNative.isNativeLoaded -> {
+ val intent =
+ Intent(
+ this@MainActivity,
+ CameraScanActivity::class.java
+ )
+ requestQRScan.launch(intent)
+ }
+ }
+ }
+ })
+ }
+ } else {
+ mToolbar?.setNavigationIcon(R.drawable.arrow_left)
+ if (getMainActivityTransform() != MainActivityTransforms.SWIPEBLE) {
+ mToolbar?.setNavigationOnClickListener {
+ openNavigationPage(
+ AbsNavigationView.SECTION_ITEM_FEED,
+ false
+ )
+ }
+ } else {
+ mToolbar?.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
+ }
+ }
+ }
+ }
+
+ internal fun onSetOffline(success: Boolean) {
+ if (success) createCustomToast(this).showToast(R.string.succ_offline) else createCustomToast(
+ this
+ ).showToastError(R.string.err_offline)
+ }
+
+ private fun onCurrentAccountChange(newAccountId: Int) {
+ mAccountId = newAccountId
+ navigationView?.onAccountChange(newAccountId)
+ Accounts.showAccountSwitchedToast(this)
+ updateNotificationCount(newAccountId)
+ if (!Settings.get().other().isDeveloper_mode) {
+ stop()
+ }
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ Logger.d(TAG, "onNewIntent, intent: $intent")
+ handleIntent(intent, false)
+ }
+
+ private fun handleIntent(intent: Intent?, isMain: Boolean): Boolean {
+ if (intent == null) {
+ return false
+ }
+ if (ACTION_OPEN_WALL == intent.action) {
+ val owner_id = intent.extras!!.getInt(Extra.OWNER_ID)
+ PlaceFactory.getOwnerWallPlace(mAccountId, owner_id, null).tryOpenWith(this)
+ return true
+ }
+ if (ACTION_SWITH_ACCOUNT == intent.action) {
+ val newAccountId = intent.extras!!.getInt(Extra.ACCOUNT_ID)
+ if (Settings.get().accounts().current != newAccountId) {
+ Settings.get()
+ .accounts().current = newAccountId
+ mAccountId = newAccountId
+ }
+ intent.action = ACTION_MAIN
+ }
+ if (ACTION_SHORTCUT_WALL == intent.action) {
+ val newAccountId = intent.extras!!.getInt(Extra.ACCOUNT_ID)
+ val ownerId = intent.extras!!.getInt(Extra.OWNER_ID)
+ if (Settings.get().accounts().current != newAccountId) {
+ Settings.get()
+ .accounts().current = newAccountId
+ mAccountId = newAccountId
+ }
+ clearBackStack()
+ openPlace(PlaceFactory.getOwnerWallPlace(newAccountId, ownerId, null))
+ return true
+ }
+ val extras = intent.extras
+ val action = intent.action
+ Logger.d(TAG, "handleIntent, extras: $extras, action: $action")
+ if (Intent.ACTION_SEND_MULTIPLE == action) {
+ val mime = intent.type
+ if (getMainActivityTransform() == MainActivityTransforms.MAIN && extras != null && mime.nonNullNoEmpty() && isMimeAudio(
+ mime
+ ) && extras.containsKey(Intent.EXTRA_STREAM)
+ ) {
+ val uris = intent.getParcelableArrayListExtraCompat(Intent.EXTRA_STREAM)
+ if (uris.nonNullNoEmpty()) {
+ val playlist = ArrayList(
+ uris.size
+ )
+ for (i in uris) {
+ val track = UploadUtils.findFileName(this, i) ?: return false
+ var TrackName = track.replace(".mp3", "")
+ var Artist = ""
+ val arr = TrackName.split(Regex(" - ")).toTypedArray()
+ if (arr.size > 1) {
+ Artist = arr[0]
+ TrackName = TrackName.replace("$Artist - ", "")
+ }
+ val tmp = Audio().setIsLocal().setThumb_image_big(
+ "share_$i"
+ ).setThumb_image_little("share_$i").setUrl(i.toString())
+ .setOwnerId(mAccountId).setArtist(Artist).setTitle(TrackName)
+ .setId(i.toString().hashCode())
+ playlist.add(tmp)
+ }
+ intent.removeExtra(Intent.EXTRA_STREAM)
+ startForPlayList(this, playlist, 0, false)
+ PlaceFactory.getPlayerPlace(mAccountId).tryOpenWith(this)
+ }
+ }
+ }
+ if (extras != null && checkInputExist(this)) {
+ mCurrentFrontSection = AbsNavigationView.SECTION_ITEM_DIALOGS
+ openNavigationPage(AbsNavigationView.SECTION_ITEM_DIALOGS, false)
+ return true
+ }
+ if (ACTION_SEND_ATTACHMENTS == action) {
+ mCurrentFrontSection = AbsNavigationView.SECTION_ITEM_DIALOGS
+ openNavigationPage(AbsNavigationView.SECTION_ITEM_DIALOGS, false)
+ return true
+ }
+ if (ACTION_OPEN_PLACE == action) {
+ val place: Place = intent.getParcelableExtraCompat(Extra.PLACE) ?: return false
+ openPlace(place)
+ return if (place.type == Place.CHAT) {
+ Settings.get().ui().swipes_chat_mode != SwipesChatMode.SLIDR || Settings.get()
+ .ui().swipes_chat_mode == SwipesChatMode.DISABLED
+ } else true
+ }
+ if (ACTION_OPEN_AUDIO_PLAYER == action) {
+ openPlace(PlaceFactory.getPlayerPlace(mAccountId))
+ return false
+ }
+ if (ACTION_CHAT_FROM_SHORTCUT == action) {
+ val aid = intent.extras!!.getInt(Extra.ACCOUNT_ID)
+ val prefsAid = Settings.get()
+ .accounts()
+ .current
+ if (prefsAid != aid) {
+ Settings.get()
+ .accounts().current = aid
+ }
+ val peerId = intent.extras!!.getInt(Extra.PEER_ID)
+ val title = intent.getStringExtra(Extra.TITLE)
+ val imgUrl = intent.getStringExtra(Extra.IMAGE)
+ val peer = Peer(peerId).setTitle(title).setAvaUrl(imgUrl)
+ PlaceFactory.getChatPlace(aid, aid, peer).tryOpenWith(this)
+ return Settings.get().ui().swipes_chat_mode != SwipesChatMode.SLIDR || Settings.get()
+ .ui().swipes_chat_mode == SwipesChatMode.DISABLED
+ }
+ if (Intent.ACTION_VIEW == action) {
+ val data = intent.data
+ val mime = intent.type ?: ""
+ if (getMainActivityTransform() == MainActivityTransforms.MAIN && mime.nonNullNoEmpty() && isMimeAudio(
+ mime
+ )
+ ) {
+ val track = UploadUtils.findFileName(this, data) ?: return false
+ var TrackName = track.replace(".mp3", "")
+ var Artist = ""
+ val arr = TrackName.split(Regex(" - ")).toTypedArray()
+ if (arr.size > 1) {
+ Artist = arr[0]
+ TrackName = TrackName.replace("$Artist - ", "")
+ }
+ val tmp =
+ Audio().setIsLocal().setThumb_image_big("share_$data").setThumb_image_little(
+ "share_$data"
+ ).setUrl(data.toString()).setOwnerId(mAccountId).setArtist(Artist)
+ .setTitle(TrackName).setId(data.toString().hashCode())
+ startForPlayList(this, ArrayList(listOf(tmp)), 0, false)
+ PlaceFactory.getPlayerPlace(mAccountId).tryOpenWith(this)
+ return false
+ }
+ LinkHelper.openUrl(this, mAccountId, data.toString(), isMain)
+ return true
+ }
+ return false
+ }
+
+ override fun setSupportActionBar(toolbar: Toolbar?) {
+ mToolbar?.setNavigationOnClickListener(null)
+ mToolbar?.setOnMenuItemClickListener(null)
+ super.setSupportActionBar(toolbar)
+ mToolbar = toolbar
+ resolveToolbarNavigationIcon()
+ }
+
+ private fun openChat(accountId: Int, messagesOwnerId: Int, peer: Peer, closeMain: Boolean) {
+ if (Settings.get().other().isEnable_show_recent_dialogs) {
+ val recentChat = RecentChat(accountId, peer.id, peer.getTitle(), peer.avaUrl)
+ navigationView?.appendRecentChat(recentChat)
+ navigationView?.refreshNavigationItems()
+ navigationView?.selectPage(recentChat)
+ }
+ if (Settings.get().ui().swipes_chat_mode == SwipesChatMode.DISABLED) {
+ val chatFragment = newInstance(accountId, messagesOwnerId, peer)
+ attachToFront(chatFragment)
+ } else {
+ if (Settings.get()
+ .ui().swipes_chat_mode == SwipesChatMode.SLIDR && getMainActivityTransform() == MainActivityTransforms.MAIN
+ ) {
+ val intent = Intent(this, ChatActivity::class.java)
+ intent.action = ChatActivity.ACTION_OPEN_PLACE
+ intent.putExtra(
+ Extra.PLACE,
+ PlaceFactory.getChatPlace(accountId, messagesOwnerId, peer)
+ )
+ startActivity(intent)
+ if (closeMain) {
+ finish()
+ overridePendingTransition(0, 0)
+ }
+ } else if (Settings.get()
+ .ui().swipes_chat_mode == SwipesChatMode.SLIDR && getMainActivityTransform() != MainActivityTransforms.MAIN
+ ) {
+ val chatFragment = newInstance(accountId, messagesOwnerId, peer)
+ attachToFront(chatFragment)
+ } else {
+ throw UnsupportedOperationException()
+ }
+ }
+ }
+
+ private fun openRecentChat(chat: RecentChat) {
+ val accountId = mAccountId
+ val messagesOwnerId = mAccountId
+ openChat(
+ accountId,
+ messagesOwnerId,
+ Peer(chat.peerId).setAvaUrl(chat.iconUrl).setTitle(chat.title),
+ false
+ )
+ }
+
+ internal fun openTargetPage() {
+ if (mTargetPage == null) {
+ return
+ }
+ val item = mTargetPage?.first ?: return
+ val clearBackStack = mTargetPage?.second ?: false
+ if (item == mCurrentFrontSection) {
+ return
+ }
+ if (item.type == AbsMenuItem.TYPE_ICON) {
+ openNavigationPage(item, clearBackStack, true)
+ }
+ if (item.type == AbsMenuItem.TYPE_RECENT_CHAT) {
+ openRecentChat(item as RecentChat)
+ }
+ mTargetPage = null
+ }
+
+ private val navigationView: AbsNavigationView?
+ get() {
+ return findViewById(R.id.additional_navigation_menu)
+ }
+
+ private fun openNavigationPage(item: AbsMenuItem, menu: Boolean) {
+ openNavigationPage(item, true, menu)
+ }
+
+ private fun startAccountsActivity() {
+ val intent = Intent(this, AccountsActivity::class.java)
+ requestLogin.launch(intent)
+ }
+
+ private fun startAccountsActivityZero() {
+ val intent = Intent(this, AccountsActivity::class.java)
+ requestLoginZero.launch(intent)
+ }
+
+ private fun clearBackStack() {
+ val manager = supportFragmentManager
+ /*if (manager.getBackStackEntryCount() > 0) {
+ FragmentManager.BackStackEntry first = manager.getBackStackEntryAt(0);
+ manager.popBackStack(first.getId(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ }*/manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+
+ // TODO: 13.12.2017 Exception java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState
+ Logger.d(TAG, "Back stack was cleared")
+ }
+
+ private fun openNavigationPage(item: AbsMenuItem, clearBackStack: Boolean, menu: Boolean) {
+ var doClearBackStack = clearBackStack
+ if (item.type == AbsMenuItem.TYPE_RECENT_CHAT) {
+ openRecentChat(item as RecentChat)
+ return
+ }
+ val sectionDrawerItem = item as SectionMenuItem
+ if (sectionDrawerItem.section == AbsNavigationView.PAGE_ACCOUNTS) {
+ startAccountsActivity()
+ return
+ }
+ mCurrentFrontSection = item
+ navigationView?.selectPage(item)
+ if (Settings.get().other().isDo_not_clear_back_stack && menu && isPlaying) {
+ doClearBackStack = !doClearBackStack
+ }
+ if (doClearBackStack) {
+ clearBackStack()
+ }
+ val aid = mAccountId
+ when (sectionDrawerItem.section) {
+ AbsNavigationView.PAGE_DIALOGS -> openPlace(
+ PlaceFactory.getDialogsPlace(
+ aid,
+ aid,
+ null
+ )
+ )
+ AbsNavigationView.PAGE_FRIENDS -> openPlace(
+ PlaceFactory.getFriendsFollowersPlace(
+ aid,
+ aid,
+ FriendsTabsFragment.TAB_ALL_FRIENDS,
+ null
+ )
+ )
+ AbsNavigationView.PAGE_GROUPS -> openPlace(
+ PlaceFactory.getCommunitiesPlace(
+ aid,
+ aid
+ )
+ )
+ AbsNavigationView.PAGE_PREFERENSES -> openPlace(PlaceFactory.getPreferencesPlace(aid))
+ AbsNavigationView.PAGE_MUSIC -> openPlace(PlaceFactory.getAudiosPlace(aid, aid))
+ AbsNavigationView.PAGE_DOCUMENTS -> openPlace(
+ PlaceFactory.getDocumentsPlace(
+ aid,
+ aid,
+ DocsListPresenter.ACTION_SHOW
+ )
+ )
+ AbsNavigationView.PAGE_FEED -> openPlace(PlaceFactory.getFeedPlace(aid))
+ AbsNavigationView.PAGE_NOTIFICATION -> openPlace(
+ PlaceFactory.getNotificationsPlace(
+ aid
+ )
+ )
+ AbsNavigationView.PAGE_PHOTOS -> openPlace(
+ PlaceFactory.getVKPhotoAlbumsPlace(
+ aid,
+ aid,
+ IVkPhotosView.ACTION_SHOW_PHOTOS,
+ null
+ )
+ )
+ AbsNavigationView.PAGE_VIDEOS -> openPlace(
+ PlaceFactory.getVideosPlace(
+ aid,
+ aid,
+ IVideosListView.ACTION_SHOW
+ )
+ )
+ AbsNavigationView.PAGE_BOOKMARKS -> openPlace(
+ PlaceFactory.getBookmarksPlace(
+ aid,
+ FaveTabsFragment.TAB_PAGES
+ )
+ )
+ AbsNavigationView.PAGE_SEARCH -> openPlace(
+ PlaceFactory.getSearchPlace(
+ aid,
+ SearchTabsFragment.TAB_PEOPLE
+ )
+ )
+ AbsNavigationView.PAGE_NEWSFEED_COMMENTS -> openPlace(
+ PlaceFactory.getNewsfeedCommentsPlace(
+ aid
+ )
+ )
+ else -> throw IllegalArgumentException("Unknown place!!! $item")
+ }
+ }
+
+ override fun onSheetItemSelected(item: AbsMenuItem, longClick: Boolean) {
+ if (mCurrentFrontSection != null && mCurrentFrontSection == item) {
+ return
+ }
+ mTargetPage = create(item, !longClick)
+ //после закрытия бокового меню откроется данная страница
+ }
+
+ override fun onSheetClosed() {
+ postResume(object : Action {
+ override fun call(target: MainActivity) {
+ target.openTargetPage()
+ }
+ })
+ }
+
+ override fun onDestroy() {
+ mCompositeDisposable.dispose()
+ isActivityDestroyed = true
+ supportFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener)
+
+ //if(!bNoDestroyServiceAudio)
+ unbindFromAudioPlayService()
+ super.onDestroy()
+ }
+
+ private fun unbindFromAudioPlayService() {
+ if (mAudioPlayServiceToken != null) {
+ unbindFromService(mAudioPlayServiceToken)
+ mAudioPlayServiceToken = null
+ }
+ }
+
+ private val isAuthValid: Boolean
+ get() = mAccountId != ISettings.IAccountsSettings.INVALID_ID
+
+ /*
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev){
+ SwipeTouchListener.getGestureDetector().onTouchEvent(ev);
+ return super.dispatchTouchEvent(ev);
+ }
+ */
+ fun keyboardHide() {
+ try {
+ val inputManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager?
+ inputManager?.hideSoftInputFromWindow(
+ window.decorView.rootView.windowToken,
+ InputMethodManager.HIDE_NOT_ALWAYS
+ )
+ } catch (ignored: Exception) {
+ }
+ }
+
+ private val frontFragment: Fragment?
+ get() = supportFragmentManager.findFragmentById(R.id.fragment)
+
+ private val isChatFragment: Boolean
+ get() = frontFragment is ChatFragment
+ private val isFragmentWithoutNavigation: Boolean
+ get() = frontFragment is CommentsFragment ||
+ frontFragment is PostCreateFragment
+
+ override fun onNavigateUp(): Boolean {
+ supportFragmentManager.popBackStack()
+ return true
+ }
+
+ /* Убрать выделение в боковом меню */
+ private fun resetNavigationSelection() {
+ mCurrentFrontSection = null
+ navigationView?.selectPage(null)
+ }
+
+ override fun onSectionResume(sectionDrawerItem: SectionMenuItem) {
+ navigationView?.selectPage(sectionDrawerItem)
+ if (mBottomNavigation != null) {
+ when (sectionDrawerItem.section) {
+ AbsNavigationView.PAGE_FEED -> mBottomNavigation?.menu?.getItem(0)?.isChecked =
+ true
+ AbsNavigationView.PAGE_SEARCH -> mBottomNavigation?.menu?.getItem(1)?.isChecked =
+ true
+ AbsNavigationView.PAGE_DIALOGS -> mBottomNavigation?.menu?.getItem(2)?.isChecked =
+ true
+ AbsNavigationView.PAGE_NOTIFICATION -> mBottomNavigation?.menu?.getItem(3)?.isChecked =
+ true
+ else -> mBottomNavigation?.menu?.getItem(4)?.isChecked = true
+ }
+ }
+ mCurrentFrontSection = sectionDrawerItem
+ }
+
+ override fun onChatResume(accountId: Int, peerId: Int, title: String?, imgUrl: String?) {
+ if (Settings.get().other().isEnable_show_recent_dialogs) {
+ val recentChat = RecentChat(accountId, peerId, title, imgUrl)
+ navigationView?.appendRecentChat(recentChat)
+ navigationView?.refreshNavigationItems()
+ navigationView?.selectPage(recentChat)
+ mCurrentFrontSection = recentChat
+ }
+ }
+
+ override fun onClearSelection() {
+ resetNavigationSelection()
+ mCurrentFrontSection = null
+ }
+
+ override fun readAllNotifications() {
+ if (Utils.isHiddenAccount(mAccountId)) return
+ mCompositeDisposable.add(
+ InteractorFactory.createFeedbackInteractor()
+ .markAsViewed(mAccountId)
+ .delay(1, TimeUnit.SECONDS)
+ .fromIOToMain()
+ .subscribe({
+ if (it) {
+ mBottomNavigation?.removeBadge(R.id.menu_feedback)
+ navigationView?.onUnreadNotificationsCountChange(0)
+ }
+ }, RxUtils.ignore())
+ )
+ }
+
+ private fun attachToFront(fragment: Fragment, animate: Boolean = true) {
+ val fragmentTransaction = supportFragmentManager.beginTransaction()
+ if (animate) fragmentTransaction.setCustomAnimations(
+ R.anim.fragment_enter,
+ R.anim.fragment_exit
+ )
+ fragmentTransaction
+ .replace(R.id.fragment, fragment)
+ .addToBackStack(null)
+ .commitAllowingStateLoss()
+ }
+
+ @Suppress("DEPRECATION")
+ override fun setStatusbarColored(colored: Boolean, invertIcons: Boolean) {
+ val statusbarNonColored = CurrentTheme.getStatusBarNonColored(this)
+ val statusbarColored = CurrentTheme.getStatusBarColor(this)
+ val w = window
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.statusBarColor = if (colored) statusbarColored else statusbarNonColored
+ @ColorInt val navigationColor =
+ if (colored) CurrentTheme.getNavigationBarColor(this) else Color.BLACK
+ w.navigationBarColor = navigationColor
+ if (Utils.hasMarshmallow()) {
+ var flags = window.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ }
+ window.decorView.systemUiVisibility = flags
+ }
+ if (Utils.hasOreo()) {
+ var flags = window.decorView.systemUiVisibility
+ if (invertIcons) {
+ flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ w.decorView.systemUiVisibility = flags
+ w.navigationBarColor = Color.WHITE
+ } else {
+ flags = flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ w.decorView.systemUiVisibility = flags
+ }
+ }
+ }
+
+ override fun hideMenu(hide: Boolean) {
+ if (hide) {
+ navigationView?.closeSheet()
+ navigationView?.blockSheet()
+ mBottomNavigationContainer?.visibility = View.GONE
+ if (Settings.get().other().is_side_navigation()) {
+ findViewById(R.id.miniplayer_side_root)?.visibility = View.GONE
+ }
+ } else {
+ mBottomNavigationContainer?.visibility = View.VISIBLE
+ if (Settings.get().other().is_side_navigation()) {
+ findViewById(R.id.miniplayer_side_root)?.visibility = View.VISIBLE
+ }
+ navigationView?.unblockSheet()
+ }
+ }
+
+ override fun openMenu(open: Boolean) {
+// if (open) {
+// getNavigationFragment().openSheet();
+// } else {
+// getNavigationFragment().closeSheet();
+// }
+ }
+
+ override fun openPlace(place: Place) {
+ val args = place.safeArguments()
+ when (place.type) {
+ Place.VIDEO_PREVIEW -> attachToFront(VideoPreviewFragment.newInstance(args))
+ Place.STORY_PLAYER -> place.launchActivityForResult(
+ this,
+ StoryPagerActivity.newInstance(this, args)
+ )
+ Place.FRIENDS_AND_FOLLOWERS -> attachToFront(FriendsTabsFragment.newInstance(args))
+ Place.EXTERNAL_LINK -> attachToFront(BrowserFragment.newInstance(args))
+ Place.DOC_PREVIEW -> {
+ val document: Document? = args.getParcelableCompat(Extra.DOC)
+ if (document != null && document.hasValidGifVideoLink()) {
+ val aid = args.getInt(Extra.ACCOUNT_ID)
+ val documents = ArrayList(listOf(document))
+ val extra = GifPagerActivity.buildArgs(aid, documents, 0)
+ place.launchActivityForResult(this, GifPagerActivity.newInstance(this, extra))
+ } else {
+ attachToFront(DocPreviewFragment.newInstance(args))
+ }
+ }
+ Place.WALL_POST -> attachToFront(WallPostFragment.newInstance(args))
+ Place.COMMENTS -> attachToFront(CommentsFragment.newInstance(place))
+ Place.WALL -> attachToFront(AbsWallFragment.newInstance(args))
+ Place.CONVERSATION_ATTACHMENTS -> attachToFront(
+ ConversationFragmentFactory.newInstance(
+ args
+ )
+ )
+ Place.PLAYER -> {
+ val player = supportFragmentManager.findFragmentByTag("audio_player")
+ if (player is AudioPlayerFragment) player.dismiss()
+ AudioPlayerFragment.newInstance(args).show(supportFragmentManager, "audio_player")
+ }
+ Place.CHAT -> {
+ val peer: Peer = args.getParcelableCompat(Extra.PEER) ?: return
+ openChat(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.OWNER_ID),
+ peer,
+ place.isNeedFinishMain
+ )
+ }
+ Place.SEARCH -> attachToFront(SearchTabsFragment.newInstance(args))
+ Place.AUDIOS_SEARCH_TABS -> attachToFront(AudioSearchTabsFragment.newInstance(args))
+ Place.GROUP_CHATS -> attachToFront(GroupChatsFragment.newInstance(args))
+ Place.BUILD_NEW_POST -> {
+ val postCreateFragment = PostCreateFragment.newInstance(args)
+ attachToFront(postCreateFragment)
+ }
+ Place.EDIT_COMMENT -> {
+ val comment: Comment? = args.getParcelableCompat(Extra.COMMENT)
+ val accountId = args.getInt(Extra.ACCOUNT_ID)
+ val commemtId = args.getInt(Extra.COMMENT_ID)
+ val commentEditFragment =
+ CommentEditFragment.newInstance(accountId, comment, commemtId)
+ place.applyFragmentListener(commentEditFragment, supportFragmentManager)
+ attachToFront(commentEditFragment)
+ }
+ Place.EDIT_POST -> {
+ val postEditFragment = PostEditFragment.newInstance(args)
+ attachToFront(postEditFragment)
+ }
+ Place.REPOST -> {
+ val repostFragment = RepostFragment.newInstance(args)
+ attachToFront(repostFragment)
+ }
+ Place.DIALOGS -> attachToFront(
+ DialogsFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.OWNER_ID),
+ args.getString(Extra.SUBTITLE)
+ )
+ )
+ Place.FORWARD_MESSAGES -> attachToFront(FwdsFragment.newInstance(args))
+ Place.TOPICS -> attachToFront(TopicsFragment.newInstance(args))
+ Place.CHAT_MEMBERS -> attachToFront(ChatMembersFragment.newInstance(args))
+ Place.FEED_BAN -> attachToFront(FeedBannedFragment.newInstance(args))
+ Place.REMOTE_FILE_MANAGER -> attachToFront(FileManagerRemoteFragment())
+ Place.COMMUNITIES -> {
+ val communitiesFragment = CommunitiesFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.USER_ID)
+ )
+ attachToFront(communitiesFragment)
+ }
+ Place.AUDIOS -> attachToFront(
+ if (Settings.get().other().isAudio_catalog_v2) CatalogV2ListFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.OWNER_ID)
+ ) else AudiosTabsFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.OWNER_ID)
+ )
+ )
+ Place.MENTIONS -> attachToFront(
+ NewsfeedMentionsFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID), args.getInt(
+ Extra.OWNER_ID
+ )
+ )
+ )
+ Place.CATALOG_V2_AUDIO_SECTION -> attachToFront(
+ CatalogV2SectionFragment.newInstance(
+ args
+ )
+ )
+ Place.CATALOG_V2_AUDIO_CATALOG -> attachToFront(CatalogV2ListFragment.newInstance(args))
+ Place.AUDIOS_IN_ALBUM -> attachToFront(AudiosFragment.newInstance(args))
+ Place.SEARCH_BY_AUDIO -> attachToFront(
+ AudiosRecommendationFragment.newInstance(
+ args.getInt(
+ Extra.ACCOUNT_ID
+ ), args.getInt(Extra.OWNER_ID), false, args.getInt(Extra.ID)
+ )
+ )
+ Place.LOCAL_SERVER_PHOTO -> attachToFront(
+ PhotosLocalServerFragment.newInstance(
+ args.getInt(
+ Extra.ACCOUNT_ID
+ )
+ )
+ )
+ Place.VIDEO_ALBUM -> attachToFront(VideosFragment.newInstance(args))
+ Place.VIDEOS -> attachToFront(VideosTabsFragment.newInstance(args))
+ Place.VK_PHOTO_ALBUMS -> attachToFront(
+ VKPhotoAlbumsFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.OWNER_ID),
+ args.getString(Extra.ACTION),
+ args.getParcelableCompat(Extra.OWNER), false
+ )
+ )
+ Place.VK_PHOTO_ALBUM -> attachToFront(VKPhotosFragment.newInstance(args))
+ Place.VK_PHOTO_ALBUM_GALLERY, Place.FAVE_PHOTOS_GALLERY, Place.SIMPLE_PHOTO_GALLERY, Place.VK_PHOTO_TMP_SOURCE, Place.VK_PHOTO_ALBUM_GALLERY_SAVED, Place.VK_PHOTO_ALBUM_GALLERY_NATIVE -> newInstance(
+ this,
+ place.type,
+ args
+ )?.let {
+ place.launchActivityForResult(
+ this,
+ it
+ )
+ }
+ Place.SINGLE_PHOTO -> place.launchActivityForResult(
+ this,
+ SinglePhotoActivity.newInstance(this, args)
+ )
+ Place.GIF_PAGER -> place.launchActivityForResult(
+ this,
+ GifPagerActivity.newInstance(this, args)
+ )
+ Place.POLL -> attachToFront(PollFragment.newInstance(args))
+ Place.BOOKMARKS -> attachToFront(FaveTabsFragment.newInstance(args))
+ Place.DOCS -> attachToFront(DocsFragment.newInstance(args))
+ Place.FEED -> attachToFront(FeedFragment.newInstance(args))
+ Place.NOTIFICATIONS -> {
+ if (Settings.get().accounts()
+ .getType(mAccountId) == AccountType.VK_ANDROID || Settings.get().accounts()
+ .getType(mAccountId) == AccountType.VK_ANDROID_HIDDEN
+ ) {
+ attachToFront(
+ FeedbackVKOfficialFragment.newInstance(
+ Settings.get().accounts().current
+ )
+ )
+ } else {
+ attachToFront(FeedbackFragment.newInstance(args))
+ }
+ }
+ Place.PREFERENCES -> attachToFront(PreferencesFragment.newInstance(args))
+ Place.RESOLVE_DOMAIN -> {
+ val domainDialog = ResolveDomainDialog.newInstance(args)
+ domainDialog.show(supportFragmentManager, "resolve-domain")
+ }
+ Place.VK_INTERNAL_PLAYER -> {
+ val intent = Intent(this, VideoPlayerActivity::class.java)
+ intent.putExtras(args)
+ startActivity(intent)
+ }
+ Place.NOTIFICATION_SETTINGS -> attachToFront(NotificationPreferencesFragment())
+ Place.LIKES_AND_COPIES -> attachToFront(LikesFragment.newInstance(args))
+ Place.CREATE_PHOTO_ALBUM, Place.EDIT_PHOTO_ALBUM -> {
+ val createPhotoAlbumFragment = CreatePhotoAlbumFragment.newInstance(args)
+ attachToFront(createPhotoAlbumFragment)
+ }
+ Place.MESSAGE_LOOKUP -> attachToFront(MessagesLookFragment.newInstance(args))
+ Place.SEARCH_COMMENTS -> attachToFront(
+ WallSearchCommentsAttachmentsFragment.newInstance(
+ args
+ )
+ )
+ Place.COMMUNITY_MEMBERS -> attachToFront(
+ CommunityMembersFragment.newInstance(args)
+ )
+ Place.UNREAD_MESSAGES -> attachToFront(NotReadMessagesFragment.newInstance(args))
+ Place.SECURITY -> attachToFront(SecurityPreferencesFragment())
+ Place.CREATE_POLL -> {
+ val createPollFragment = CreatePollFragment.newInstance(args)
+ place.applyFragmentListener(createPollFragment, supportFragmentManager)
+ attachToFront(createPollFragment)
+ }
+ Place.COMMENT_CREATE -> openCommentCreatePlace(place)
+ Place.LOGS -> attachToFront(LogsFragment.newInstance())
+ Place.SINGLE_SEARCH -> {
+ val singleTabSearchFragment = SingleTabSearchFragment.newInstance(args)
+ attachToFront(singleTabSearchFragment)
+ }
+ Place.NEWSFEED_COMMENTS -> {
+ val newsfeedCommentsFragment = NewsfeedCommentsFragment.newInstance(
+ args.getInt(
+ Extra.ACCOUNT_ID
+ )
+ )
+ attachToFront(newsfeedCommentsFragment)
+ }
+ Place.COMMUNITY_CONTROL -> {
+ val communityControlFragment = CommunityControlFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getParcelableCompat(Extra.OWNER),
+ args.getParcelableCompat(Extra.SETTINGS)
+ )
+ attachToFront(communityControlFragment)
+ }
+ Place.COMMUNITY_INFO -> {
+ val communityInfoFragment = CommunityInfoContactsFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getParcelableCompat(Extra.OWNER)
+ )
+ attachToFront(communityInfoFragment)
+ }
+ Place.COMMUNITY_INFO_LINKS -> {
+ val communityLinksFragment = CommunityInfoLinksFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getParcelableCompat(Extra.OWNER)
+ )
+ attachToFront(communityLinksFragment)
+ }
+ Place.SETTINGS_THEME -> {
+ val themes = ThemeFragment()
+ attachToFront(themes)
+ if (navigationView?.isSheetOpen == true) {
+ navigationView?.closeSheet()
+ return
+ }
+ }
+ Place.COMMUNITY_BAN_EDIT -> {
+ val communityBanEditFragment = CommunityBanEditFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.GROUP_ID),
+ args.getParcelableCompat(Extra.BANNED)
+ )
+ attachToFront(communityBanEditFragment)
+ }
+ Place.COMMUNITY_ADD_BAN -> attachToFront(
+ CommunityBanEditFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.GROUP_ID),
+ args.getParcelableArrayListCompat(Extra.USERS)
+ )
+ )
+ Place.COMMUNITY_MANAGER_ADD -> attachToFront(
+ CommunityManagerEditFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.GROUP_ID),
+ args.getParcelableArrayListCompat(Extra.USERS)
+ )
+ )
+ Place.COMMUNITY_MANAGER_EDIT -> attachToFront(
+ CommunityManagerEditFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.GROUP_ID),
+ args.getParcelableCompat(Extra.MANAGER)
+ )
+ )
+ Place.REQUEST_EXECUTOR -> attachToFront(
+ RequestExecuteFragment.newInstance(
+ args.getInt(
+ Extra.ACCOUNT_ID
+ )
+ )
+ )
+ Place.USER_BLACKLIST -> attachToFront(UserBannedFragment.newInstance(args.getInt(Extra.ACCOUNT_ID)))
+ Place.FRIENDS_BIRTHDAYS -> attachToFront(BirthDayFragment.newInstance(args))
+ Place.DRAWER_EDIT -> attachToFront(DrawerEditFragment.newInstance())
+ Place.SIDE_DRAWER_EDIT -> attachToFront(SideDrawerEditFragment.newInstance())
+ Place.CATALOG_V2_LIST_EDIT -> attachToFront(CatalogV2ListEditFragment.newInstance())
+ Place.ARTIST -> {
+ attachToFront(AudiosByArtistFragment.newInstance(args))
+ }
+ Place.SHORT_LINKS -> attachToFront(ShortedLinksFragment.newInstance(args.getInt(Extra.ACCOUNT_ID)))
+
+ Place.SHORTCUTS -> attachToFront(ShortcutsViewFragment())
+ Place.IMPORTANT_MESSAGES -> attachToFront(
+ ImportantMessagesFragment.newInstance(
+ args.getInt(
+ Extra.ACCOUNT_ID
+ )
+ )
+ )
+ Place.OWNER_ARTICLES -> attachToFront(
+ OwnerArticlesFragment.newInstance(
+ args.getInt(
+ Extra.ACCOUNT_ID
+ ), args.getInt(Extra.OWNER_ID)
+ )
+ )
+ Place.USER_DETAILS -> {
+ val accountId = args.getInt(Extra.ACCOUNT_ID)
+ val user: User = args.getParcelableCompat(Extra.USER) ?: return
+ val details: UserDetails = args.getParcelableCompat("details") ?: return
+ attachToFront(newInstance(accountId, user, details))
+ }
+ Place.WALL_ATTACHMENTS -> {
+ val wall_attachments = WallAttachmentsFragmentFactory.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.OWNER_ID),
+ args.getString(Extra.TYPE)
+ )
+ ?: throw IllegalArgumentException("wall_attachments cant bee null")
+ attachToFront(wall_attachments)
+ }
+ Place.MARKET_ALBUMS -> attachToFront(
+ ProductAlbumsFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.OWNER_ID)
+ )
+ )
+ Place.NARRATIVES -> attachToFront(
+ NarrativesFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.OWNER_ID)
+ )
+ )
+ Place.MARKETS -> attachToFront(
+ ProductsFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.OWNER_ID),
+ args.getInt(Extra.ALBUM_ID),
+ args.getBoolean(Extra.SERVICE)
+ )
+ )
+ Place.PHOTO_ALL_COMMENT -> attachToFront(
+ PhotoAllCommentFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.OWNER_ID)
+ )
+ )
+ Place.GIFTS -> attachToFront(
+ GiftsFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.OWNER_ID)
+ )
+ )
+ Place.MARKET_VIEW -> attachToFront(MarketViewFragment.newInstance(args))
+ Place.ALBUMS_BY_VIDEO -> attachToFront(VideoAlbumsByVideoFragment.newInstance(args))
+ Place.FRIENDS_BY_PHONES -> attachToFront(FriendsByPhonesFragment.newInstance(args))
+ Place.VOTERS -> attachToFront(VotersFragment.newInstance(args))
+ else -> throw IllegalArgumentException("Main activity can't open this place, type: " + place.type)
+ }
+ }
+
+ private fun openCommentCreatePlace(place: Place) {
+ val args = place.safeArguments()
+ val fragment = CommentCreateFragment.newInstance(
+ args.getInt(Extra.ACCOUNT_ID),
+ args.getInt(Extra.COMMENT_ID),
+ args.getInt(Extra.OWNER_ID),
+ args.getString(Extra.BODY)
+ )
+ place.applyFragmentListener(fragment, supportFragmentManager)
+ attachToFront(fragment)
+ }
+
+ override fun onServiceConnected(name: ComponentName, service: IBinder) {
+ if (name.className == MusicPlaybackService::class.java.name) {
+ Logger.d(TAG, "Connected to MusicPlaybackService")
+ }
+ }
+
+ override fun onServiceDisconnected(name: ComponentName) {
+ if (isActivityDestroyed) return
+ if (name.className == MusicPlaybackService::class.java.name) {
+ Logger.d(TAG, "Disconnected from MusicPlaybackService")
+ mAudioPlayServiceToken = null
+ bindToAudioPlayService()
+ }
+ }
+
+ private fun openPageAndCloseSheet(item: AbsMenuItem) {
+ if (navigationView?.isSheetOpen == true) {
+ navigationView?.closeSheet()
+ onSheetItemSelected(item, false)
+ } else {
+ openNavigationPage(item, true)
+ }
+ }
+
+ private fun updateMessagesBagde(count: Int) {
+ navigationView?.onUnreadDialogsCountChange(count)
+ if (mBottomNavigation != null) {
+ if (count > 0) {
+ val badgeDrawable = mBottomNavigation?.getOrCreateBadge(R.id.menu_messages)
+ badgeDrawable?.isBadgeNotSaveColor = true
+ badgeDrawable?.number = count
+ } else {
+ mBottomNavigation?.removeBadge(R.id.menu_messages)
+ }
+ }
+ }
+
+ private fun updateNotificationsBadge(counters: SectionCounters) {
+ navigationView?.onUnreadDialogsCountChange(counters.messages)
+ navigationView?.onUnreadNotificationsCountChange(counters.notifications)
+ if (mBottomNavigation != null) {
+ if (counters.notifications > 0) {
+ val badgeDrawable = mBottomNavigation?.getOrCreateBadge(R.id.menu_feedback)
+ badgeDrawable?.isBadgeNotSaveColor = true
+ badgeDrawable?.number = counters.notifications
+ } else {
+ mBottomNavigation?.removeBadge(R.id.menu_feedback)
+ }
+ if (counters.messages > 0) {
+ val badgeDrawable = mBottomNavigation?.getOrCreateBadge(R.id.menu_messages)
+ badgeDrawable?.isBadgeNotSaveColor = true
+ badgeDrawable?.number = counters.messages
+ } else {
+ mBottomNavigation?.removeBadge(R.id.menu_messages)
+ }
+ }
+ }
+
+ private fun removeNotificationsBadge() {
+ navigationView?.onUnreadDialogsCountChange(0)
+ navigationView?.onUnreadNotificationsCountChange(0)
+ mBottomNavigation?.removeBadge(R.id.menu_feedback)
+ mBottomNavigation?.removeBadge(R.id.menu_messages)
+ }
+
+ override fun onNavigationItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.menu_feed -> {
+ openPageAndCloseSheet(AbsNavigationView.SECTION_ITEM_FEED)
+ return true
+ }
+ R.id.menu_search -> {
+ openPageAndCloseSheet(AbsNavigationView.SECTION_ITEM_SEARCH)
+ return true
+ }
+ R.id.menu_messages -> {
+ openPageAndCloseSheet(AbsNavigationView.SECTION_ITEM_DIALOGS)
+ return true
+ }
+ R.id.menu_feedback -> {
+ openPageAndCloseSheet(AbsNavigationView.SECTION_ITEM_FEEDBACK)
+ return true
+ }
+ R.id.menu_other -> {
+ if (navigationView?.isSheetOpen == true) {
+ navigationView?.closeSheet()
+ } else {
+ navigationView?.openSheet()
+ }
+ return true
+ }
+ else -> return false
+ }
+ }
+
+ override fun onUpdateNavigation() {
+ resolveToolbarNavigationIcon()
+ }
+
+ protected val DOUBLE_BACK_PRESSED_TIMEOUT = 2000
+
+ companion object {
+ const val ACTION_MAIN = "android.intent.action.MAIN"
+ const val ACTION_CHAT_FROM_SHORTCUT = "dev.ragnarok.fenrir.ACTION_CHAT_FROM_SHORTCUT"
+ const val ACTION_OPEN_PLACE = "dev.ragnarok.fenrir.activity.MainActivity.openPlace"
+ const val ACTION_OPEN_AUDIO_PLAYER =
+ "dev.ragnarok.fenrir.activity.MainActivity.openAudioPlayer"
+ const val ACTION_SEND_ATTACHMENTS = "dev.ragnarok.fenrir.ACTION_SEND_ATTACHMENTS"
+ const val ACTION_SWITH_ACCOUNT = "dev.ragnarok.fenrir.ACTION_SWITH_ACCOUNT"
+ const val ACTION_SHORTCUT_WALL = "dev.ragnarok.fenrir.ACTION_SHORTCUT_WALL"
+ const val ACTION_OPEN_WALL = "dev.ragnarok.fenrir.ACTION_OPEN_WALL"
+ const val EXTRA_NO_REQUIRE_PIN = "no_require_pin"
+
+ /**
+ * Extra with type [dev.ragnarok.fenrir.model.ModelsBundle] only
+ */
+ const val EXTRA_INPUT_ATTACHMENTS = "input_attachments"
+ private const val TAG = "MainActivity_LOG"
+
+ /**
+ * Check the device to make sure it has the Google Play Services APK. If
+ * it doesn't, display a dialog that allows users to download the APK from
+ * the Google Play Store or enable it in the device's system settings.
+ */
+ fun checkPlayServices(context: Context): Boolean {
+ val apiAvailability = GoogleApiAvailability.getInstance()
+ val resultCode = apiAvailability.isGooglePlayServicesAvailable(context)
+ return resultCode == ConnectionResult.SUCCESS
+ }
+ }
+}
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/NoMainActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/NoMainActivity.kt
new file mode 100644
index 000000000..b43d7a4b8
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/NoMainActivity.kt
@@ -0,0 +1,100 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.os.Bundle
+import android.view.MotionEvent
+import android.view.WindowManager
+import androidx.activity.OnBackPressedCallback
+import androidx.annotation.IdRes
+import androidx.annotation.LayoutRes
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import androidx.fragment.app.FragmentManager
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.listener.BackPressCallback
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.settings.theme.ThemesController.currentStyle
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.view.zoomhelper.ZoomHelper.Companion.getInstance
+
+abstract class NoMainActivity : AppCompatActivity() {
+ private var mToolbar: Toolbar? = null
+ private val mBackStackListener =
+ FragmentManager.OnBackStackChangedListener { resolveToolbarNavigationIcon() }
+ private var isZoomPhoto = false
+
+ @Suppress("DEPRECATION")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ setTheme(currentStyle())
+ Utils.prepareDensity(this)
+ super.onCreate(savedInstanceState)
+ isZoomPhoto = Settings.get().other().isDo_zoom_photo
+ setContentView(getNoMainContentView())
+ val w = window
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.statusBarColor = CurrentTheme.getStatusBarColor(this)
+ w.navigationBarColor = CurrentTheme.getNavigationBarColor(this)
+ supportFragmentManager.addOnBackStackChangedListener(mBackStackListener)
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ val fm = supportFragmentManager
+ val front = fm.findFragmentById(
+ getMainContainerViewId()
+ )
+ if (front is BackPressCallback) {
+ if (!(front as BackPressCallback).onBackPressed()) {
+ return
+ }
+ }
+ if (fm.backStackEntryCount <= 1) {
+ supportFinishAfterTransition()
+ } else {
+ supportFragmentManager.popBackStack()
+ }
+ }
+ })
+ }
+
+ override fun attachBaseContext(newBase: Context) {
+ super.attachBaseContext(Utils.updateActivityContext(newBase))
+ }
+
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+ return if (!isZoomPhoto) {
+ super.dispatchTouchEvent(ev)
+ } else getInstance()?.dispatchTouchEvent(ev, this) == true || super.dispatchTouchEvent(ev)
+ }
+
+ @LayoutRes
+ protected open fun getNoMainContentView(): Int {
+ return R.layout.activity_no_main
+ }
+
+ @IdRes
+ protected open fun getMainContainerViewId(): Int {
+ return R.id.fragment
+ }
+
+ override fun setSupportActionBar(toolbar: Toolbar?) {
+ super.setSupportActionBar(toolbar)
+ mToolbar = toolbar
+ resolveToolbarNavigationIcon()
+ }
+
+ private fun resolveToolbarNavigationIcon() {
+ val manager = supportFragmentManager
+ if (manager.backStackEntryCount > 1) {
+ mToolbar?.setNavigationIcon(R.drawable.arrow_left)
+ } else {
+ mToolbar?.setNavigationIcon(R.drawable.close)
+ }
+ mToolbar?.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
+ }
+
+ override fun onDestroy() {
+ supportFragmentManager.removeOnBackStackChangedListener(mBackStackListener)
+ super.onDestroy()
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/NotReadMessagesActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/NotReadMessagesActivity.kt
new file mode 100644
index 000000000..faae6ad75
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/NotReadMessagesActivity.kt
@@ -0,0 +1,206 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.view.View
+import android.view.WindowManager
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.ColorInt
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.gifpager.GifPagerActivity
+import dev.ragnarok.fenrir.activity.photopager.PhotoPagerActivity.Companion.newInstance
+import dev.ragnarok.fenrir.activity.slidr.Slidr.attach
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrListener
+import dev.ragnarok.fenrir.activity.storypager.StoryPagerActivity
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment.Companion.newInstance
+import dev.ragnarok.fenrir.fragment.messages.chat.ChatFragment.Companion.newInstance
+import dev.ragnarok.fenrir.fragment.messages.notreadmessages.NotReadMessagesFragment
+import dev.ragnarok.fenrir.getParcelableCompat
+import dev.ragnarok.fenrir.getParcelableExtraCompat
+import dev.ragnarok.fenrir.listener.AppStyleable
+import dev.ragnarok.fenrir.model.Document
+import dev.ragnarok.fenrir.model.Peer
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceProvider
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.ViewUtils
+
+class NotReadMessagesActivity : NoMainActivity(), PlaceProvider, AppStyleable {
+ //resolveToolbarNavigationIcon();
+ private val mOnBackStackChangedListener =
+ FragmentManager.OnBackStackChangedListener { keyboardHide() }
+ internal val frontFragment: Fragment?
+ get() = supportFragmentManager.findFragmentById(R.id.fragment)
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ attach(this, SlidrConfig.Builder().listener(object : SlidrListener {
+ override fun onSlideStateChanged(state: Int) {}
+ override fun onSlideChange(percent: Float) {}
+ override fun onSlideOpened() {}
+ override fun onSlideClosed(): Boolean {
+ val fragment = frontFragment
+ if (fragment is NotReadMessagesFragment) {
+ fragment.fireFinish()
+ } else {
+ return false
+ }
+ return true
+ }
+ }).scrimColor(CurrentTheme.getColorBackground(this)).build())
+ if (savedInstanceState == null) {
+ handleIntent(intent)
+ supportFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener)
+ }
+ }
+
+ private fun handleIntent(intent: Intent?) {
+ if (intent == null) {
+ finish()
+ return
+ }
+ val action = intent.action
+ if (ACTION_OPEN_PLACE == action) {
+ val place: Place? = intent.getParcelableExtraCompat(Extra.PLACE)
+ if (place == null) {
+ finish()
+ return
+ }
+ openPlace(place)
+ }
+ }
+
+ fun keyboardHide() {
+ try {
+ val inputManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager?
+ inputManager?.hideSoftInputFromWindow(
+ window.decorView.rootView.windowToken,
+ InputMethodManager.HIDE_NOT_ALWAYS
+ )
+ } catch (ignored: Exception) {
+ }
+ }
+
+ override fun openPlace(place: Place) {
+ val args = place.safeArguments()
+ when (place.type) {
+ Place.CHAT -> {
+ val peer: Peer = args.getParcelableCompat(Extra.PEER) ?: return
+ val chatFragment =
+ newInstance(args.getInt(Extra.ACCOUNT_ID), args.getInt(Extra.OWNER_ID), peer)
+ attachToFront(chatFragment)
+ }
+ Place.UNREAD_MESSAGES -> attachToFront(NotReadMessagesFragment.newInstance(args))
+ Place.VK_PHOTO_ALBUM_GALLERY, Place.FAVE_PHOTOS_GALLERY, Place.SIMPLE_PHOTO_GALLERY, Place.VK_PHOTO_TMP_SOURCE, Place.VK_PHOTO_ALBUM_GALLERY_SAVED, Place.VK_PHOTO_ALBUM_GALLERY_NATIVE -> newInstance(
+ this,
+ place.type,
+ args
+ )?.let {
+ place.launchActivityForResult(
+ this,
+ it
+ )
+ }
+ Place.STORY_PLAYER -> place.launchActivityForResult(
+ this,
+ StoryPagerActivity.newInstance(this, args)
+ )
+ Place.SINGLE_PHOTO -> place.launchActivityForResult(
+ this,
+ SinglePhotoActivity.newInstance(this, args)
+ )
+ Place.GIF_PAGER -> place.launchActivityForResult(
+ this,
+ GifPagerActivity.newInstance(this, args)
+ )
+ Place.DOC_PREVIEW -> {
+ val document: Document? = args.getParcelableCompat(Extra.DOC)
+ if (document != null && document.hasValidGifVideoLink()) {
+ val aid = args.getInt(Extra.ACCOUNT_ID)
+ val documents = ArrayList(listOf(document))
+ val extra = GifPagerActivity.buildArgs(aid, documents, 0)
+ place.launchActivityForResult(this, GifPagerActivity.newInstance(this, extra))
+ } else {
+ Utils.openPlaceWithSwipebleActivity(this, place)
+ }
+ }
+ Place.PLAYER -> {
+ val player = supportFragmentManager.findFragmentByTag("audio_player")
+ if (player is AudioPlayerFragment) player.dismiss()
+ newInstance(args).show(supportFragmentManager, "audio_player")
+ }
+ else -> Utils.openPlaceWithSwipebleActivity(this, place)
+ }
+ }
+
+ private fun attachToFront(fragment: Fragment, animate: Boolean = true) {
+ val fragmentTransaction = supportFragmentManager.beginTransaction()
+ if (animate) fragmentTransaction.setCustomAnimations(
+ R.anim.fragment_enter,
+ R.anim.fragment_exit
+ )
+ fragmentTransaction
+ .replace(R.id.fragment, fragment)
+ .addToBackStack(null)
+ .commitAllowingStateLoss()
+ }
+
+ public override fun onPause() {
+ ViewUtils.keyboardHide(this)
+ super.onPause()
+ }
+
+ public override fun onDestroy() {
+ supportFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener)
+ ViewUtils.keyboardHide(this)
+ super.onDestroy()
+ }
+
+ override fun hideMenu(hide: Boolean) {}
+ override fun openMenu(open: Boolean) {}
+
+ @Suppress("DEPRECATION")
+ override fun setStatusbarColored(colored: Boolean, invertIcons: Boolean) {
+ val statusbarNonColored = CurrentTheme.getStatusBarNonColored(this)
+ val statusbarColored = CurrentTheme.getStatusBarColor(this)
+ val w = window
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.statusBarColor = if (colored) statusbarColored else statusbarNonColored
+ @ColorInt val navigationColor =
+ if (colored) CurrentTheme.getNavigationBarColor(this) else Color.BLACK
+ w.navigationBarColor = navigationColor
+ if (Utils.hasMarshmallow()) {
+ var flags = window.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ }
+ window.decorView.systemUiVisibility = flags
+ }
+ if (Utils.hasOreo()) {
+ var flags = window.decorView.systemUiVisibility
+ if (invertIcons) {
+ flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ w.decorView.systemUiVisibility = flags
+ w.navigationBarColor = Color.WHITE
+ } else {
+ flags = flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ w.decorView.systemUiVisibility = flags
+ }
+ }
+ }
+
+ companion object {
+ const val ACTION_OPEN_PLACE =
+ "dev.ragnarok.fenrir.activity.NotReadMessagesActivity.openPlace"
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/PhotoAlbumsActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/PhotoAlbumsActivity.kt
new file mode 100644
index 000000000..dac715ee9
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/PhotoAlbumsActivity.kt
@@ -0,0 +1,41 @@
+package dev.ragnarok.fenrir.activity
+
+import android.os.Bundle
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.fragment.vkphotoalbums.VKPhotoAlbumsFragment
+import dev.ragnarok.fenrir.fragment.vkphotos.VKPhotosFragment
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceProvider
+
+class PhotoAlbumsActivity : NoMainActivity(), PlaceProvider {
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ val intent = intent
+ val accountId = (intent.extras ?: return).getInt(Extra.ACCOUNT_ID)
+ val ownerId = (intent.extras ?: return).getInt(Extra.OWNER_ID)
+ val action = intent.getStringExtra(Extra.ACTION)
+ val fragment =
+ VKPhotoAlbumsFragment.newInstance(accountId, ownerId, action, null, false)
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter, R.anim.fragment_exit)
+ .add(R.id.fragment, fragment)
+ .addToBackStack(null)
+ .commit()
+ }
+ }
+
+ override fun openPlace(place: Place) {
+ if (place.type == Place.VK_PHOTO_ALBUM) {
+ val fragment = VKPhotosFragment.newInstance(place.safeArguments())
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(R.id.fragment, fragment)
+ .addToBackStack("photos")
+ .commit()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/PhotosActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/PhotosActivity.kt
new file mode 100644
index 000000000..fcce4f0cf
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/PhotosActivity.kt
@@ -0,0 +1,55 @@
+package dev.ragnarok.fenrir.activity
+
+import android.os.Bundle
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.fragment.localimagealbums.LocalImageAlbumsFragment
+import dev.ragnarok.fenrir.fragment.localphotos.LocalPhotosFragment
+import dev.ragnarok.fenrir.getParcelableCompat
+import dev.ragnarok.fenrir.model.LocalImageAlbum
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceProvider
+
+class PhotosActivity : NoMainActivity(), PlaceProvider {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ attachAlbumsFragment()
+ }
+ }
+
+ private fun attachAlbumsFragment() {
+ val ignoredFragment = LocalImageAlbumsFragment()
+ ignoredFragment.arguments = intent.extras
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter, R.anim.fragment_exit)
+ .replace(R.id.fragment, ignoredFragment)
+ .addToBackStack(null)
+ .commit()
+ }
+
+ override fun openPlace(place: Place) {
+ if (place.type == Place.LOCAL_IMAGE_ALBUM) {
+ val maxSelectionCount = intent.getIntExtra(EXTRA_MAX_SELECTION_COUNT, 10)
+ val album: LocalImageAlbum? = place.safeArguments().getParcelableCompat(Extra.ALBUM)
+ val localPhotosFragment =
+ LocalPhotosFragment.newInstance(maxSelectionCount, album, false)
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(R.id.fragment, localPhotosFragment)
+ .addToBackStack("photos")
+ .commit()
+ } else if (place.type == Place.SINGLE_PHOTO) {
+ place.launchActivityForResult(
+ this,
+ SinglePhotoActivity.newInstance(this, place.safeArguments())
+ )
+ }
+ }
+
+ companion object {
+ const val EXTRA_MAX_SELECTION_COUNT = "max_selection_count"
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/PostCreateActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/PostCreateActivity.kt
new file mode 100644
index 000000000..618323bad
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/PostCreateActivity.kt
@@ -0,0 +1,62 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.fragment.attachments.postcreate.PostCreateFragment
+import dev.ragnarok.fenrir.getParcelableArrayListExtraCompat
+import dev.ragnarok.fenrir.getParcelableExtraCompat
+import dev.ragnarok.fenrir.model.EditingPostType
+import dev.ragnarok.fenrir.model.WallEditorAttrs
+
+class PostCreateActivity : NoMainActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ val accountId = (intent.extras ?: return).getInt(Extra.ACCOUNT_ID)
+ val streams = intent.getParcelableArrayListExtraCompat("streams")
+ val attrs: WallEditorAttrs = intent.getParcelableExtraCompat("attrs") ?: return
+ val links = intent.getStringExtra("links")
+ val mime = intent.getStringExtra(Extra.TYPE)
+ val args = PostCreateFragment.buildArgs(
+ accountId,
+ attrs.getOwner().ownerId,
+ EditingPostType.TEMP,
+ null,
+ attrs,
+ streams,
+ links,
+ mime
+ )
+ val fragment = PostCreateFragment.newInstance(args)
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter, R.anim.fragment_exit)
+ .replace(getMainContainerViewId(), fragment)
+ .addToBackStack(null)
+ .commitAllowingStateLoss()
+ }
+ }
+
+ companion object {
+
+ fun newIntent(
+ context: Context,
+ accountId: Int,
+ attrs: WallEditorAttrs,
+ streams: ArrayList?,
+ links: String?,
+ mime: String?
+ ): Intent {
+ return Intent(context, PostCreateActivity::class.java)
+ .putExtra(Extra.ACCOUNT_ID, accountId)
+ .putParcelableArrayListExtra("streams", streams)
+ .putExtra("attrs", attrs)
+ .putExtra("links", links)
+ .putExtra(Extra.TYPE, mime)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/PostPublishPrepareActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/PostPublishPrepareActivity.kt
new file mode 100644
index 000000000..a5751b1dd
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/PostPublishPrepareActivity.kt
@@ -0,0 +1,151 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.ActivityUtils.StreamData
+import dev.ragnarok.fenrir.activity.PostCreateActivity.Companion.newIntent
+import dev.ragnarok.fenrir.domain.IOwnersRepository
+import dev.ragnarok.fenrir.domain.Repository.owners
+import dev.ragnarok.fenrir.fragment.base.RecyclerMenuAdapter
+import dev.ragnarok.fenrir.fromIOToMain
+import dev.ragnarok.fenrir.model.Icon
+import dev.ragnarok.fenrir.model.Owner
+import dev.ragnarok.fenrir.model.Text
+import dev.ragnarok.fenrir.model.WallEditorAttrs
+import dev.ragnarok.fenrir.model.menu.AdvancedItem
+import dev.ragnarok.fenrir.settings.ISettings
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.settings.theme.ThemesController.currentStyle
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.toast.CustomToast
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+
+class PostPublishPrepareActivity : AppCompatActivity(), RecyclerMenuAdapter.ActionListener {
+ private val compositeDisposable = CompositeDisposable()
+ private var adapter: RecyclerMenuAdapter? = null
+ private var recyclerView: RecyclerView? = null
+ private var proressView: View? = null
+ private var streams: StreamData? = null
+ private var links: String? = null
+ private var mime: String? = null
+ private var accountId = 0
+ private var loading = false
+ override fun attachBaseContext(newBase: Context) {
+ super.attachBaseContext(Utils.updateActivityContext(newBase))
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ setTheme(currentStyle())
+ Utils.prepareDensity(this)
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_post_publish_prepare)
+ adapter = RecyclerMenuAdapter(R.layout.item_advanced_menu_alternative, emptyList())
+ adapter?.setActionListener(this)
+ recyclerView = findViewById(R.id.recycler_view)
+ recyclerView?.layoutManager = LinearLayoutManager(this)
+ recyclerView?.adapter = adapter
+ proressView = findViewById(R.id.progress_view)
+ if (savedInstanceState == null) {
+ accountId = Settings.get().accounts().current
+ if (accountId == ISettings.IAccountsSettings.INVALID_ID) {
+ CustomToast.createCustomToast(this).setDuration(Toast.LENGTH_LONG)
+ .showToastError(R.string.error_post_creation_no_auth)
+ finish()
+ }
+ streams = ActivityUtils.checkLocalStreams(this)
+ mime = streams?.mime
+ links = ActivityUtils.checkLinks(this)
+ setLoading(true)
+ val interactor = owners
+ compositeDisposable.add(interactor.getCommunitiesWhereAdmin(
+ accountId,
+ admin = true,
+ editor = true,
+ moderator = false
+ )
+ .zipWith>(
+ interactor.getBaseOwnerInfo(
+ accountId,
+ accountId,
+ IOwnersRepository.MODE_NET
+ )
+ ) { owners: List, owner: Owner ->
+ val result: MutableList = ArrayList()
+ result.add(owner)
+ result.addAll(owners)
+ result
+ }
+ .fromIOToMain()
+ .subscribe({ owners -> onOwnersReceived(owners) }) { throwable ->
+ onOwnersGetError(
+ throwable
+ )
+ })
+ }
+ updateViews()
+ }
+
+ private fun onOwnersGetError(throwable: Throwable) {
+ setLoading(false)
+ CustomToast.createCustomToast(this).setDuration(Toast.LENGTH_LONG)
+ .showToastError(Utils.firstNonEmptyString(throwable.message, throwable.toString()))
+ finish()
+ }
+
+ private fun onOwnersReceived(owners: List) {
+ setLoading(false)
+ if (owners.isEmpty()) {
+ finish() // wtf???
+ return
+ }
+ val iam = owners[0]
+ val items: MutableList = ArrayList()
+ for (owner in owners) {
+ val attrs = WallEditorAttrs(owner, iam)
+ items.add(
+ AdvancedItem(owner.ownerId, Text(owner.fullName))
+ .setIcon(Icon.fromUrl(owner.get100photoOrSmaller()))
+ .setSubtitle(Text("@" + owner.domain))
+ .setTag(attrs)
+ )
+ }
+ adapter?.setItems(items)
+ }
+
+ private fun setLoading(loading: Boolean) {
+ this.loading = loading
+ updateViews()
+ }
+
+ private fun updateViews() {
+ recyclerView?.visibility = if (loading) View.GONE else View.VISIBLE
+ proressView?.visibility = if (loading) View.VISIBLE else View.GONE
+ }
+
+ override fun onDestroy() {
+ compositeDisposable.dispose()
+ super.onDestroy()
+ }
+
+ override fun onClick(item: AdvancedItem) {
+ val attrs = item.tag as WallEditorAttrs
+ val intent = newIntent(
+ this,
+ accountId,
+ attrs,
+ streams?.uris,
+ links,
+ mime
+ )
+ startActivity(intent)
+ finish()
+ }
+
+ override fun onLongClick(item: AdvancedItem) {}
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ProfileSelectable.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ProfileSelectable.kt
new file mode 100644
index 000000000..cb0f25fc1
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ProfileSelectable.kt
@@ -0,0 +1,9 @@
+package dev.ragnarok.fenrir.activity
+
+import dev.ragnarok.fenrir.model.Owner
+import dev.ragnarok.fenrir.model.SelectProfileCriteria
+
+interface ProfileSelectable {
+ fun select(owner: Owner)
+ val acceptableCriteria: SelectProfileCriteria?
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ProxyManagerActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ProxyManagerActivity.kt
new file mode 100644
index 000000000..d87199ca7
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ProxyManagerActivity.kt
@@ -0,0 +1,33 @@
+package dev.ragnarok.fenrir.activity
+
+import android.os.Bundle
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.fragment.addproxy.AddProxyFragment
+import dev.ragnarok.fenrir.fragment.proxymanager.ProxyManagerFrgament
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceProvider
+
+class ProxyManagerActivity : NoMainActivity(), PlaceProvider {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(getMainContainerViewId(), ProxyManagerFrgament.newInstance())
+ .addToBackStack("proxy-manager")
+ .commit()
+ }
+ }
+
+ override fun openPlace(place: Place) {
+ if (place.type == Place.PROXY_ADD) {
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(getMainContainerViewId(), AddProxyFragment.newInstance())
+ .addToBackStack("proxy-add")
+ .commit()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/QuickAnswerActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/QuickAnswerActivity.kt
new file mode 100644
index 000000000..0ff52df8d
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/QuickAnswerActivity.kt
@@ -0,0 +1,354 @@
+package dev.ragnarok.fenrir.activity
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.text.Editable
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.TextView
+import android.widget.Toast
+import androidx.annotation.StyleRes
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import com.google.android.material.textfield.TextInputEditText
+import dev.ragnarok.fenrir.*
+import dev.ragnarok.fenrir.crypt.KeyLocationPolicy
+import dev.ragnarok.fenrir.domain.IMessagesRepository
+import dev.ragnarok.fenrir.domain.Repository.messages
+import dev.ragnarok.fenrir.fragment.base.AttachmentsHolder
+import dev.ragnarok.fenrir.fragment.base.AttachmentsViewBinder
+import dev.ragnarok.fenrir.fragment.base.AttachmentsViewBinder.OnAttachmentsActionCallback
+import dev.ragnarok.fenrir.fragment.base.AttachmentsViewBinder.VoiceActionListener
+import dev.ragnarok.fenrir.link.LinkHelper
+import dev.ragnarok.fenrir.listener.TextWatcherAdapter
+import dev.ragnarok.fenrir.longpoll.NotificationHelper
+import dev.ragnarok.fenrir.media.music.MusicPlaybackService.Companion.startForPlayList
+import dev.ragnarok.fenrir.model.*
+import dev.ragnarok.fenrir.place.PlaceFactory
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.settings.theme.ThemeOverlay
+import dev.ragnarok.fenrir.util.AppPerms.requestPermissionsAbs
+import dev.ragnarok.fenrir.util.AppTextUtils
+import dev.ragnarok.fenrir.util.TextingNotifier
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.ViewUtils
+import dev.ragnarok.fenrir.util.rxutils.RxUtils
+import dev.ragnarok.fenrir.util.toast.CustomToast.Companion.createCustomToast
+import dev.ragnarok.fenrir.view.emoji.BotKeyboardView
+import dev.ragnarok.fenrir.view.emoji.BotKeyboardView.BotKeyboardViewDelegate
+import io.reactivex.rxjava3.core.Observable
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+import java.util.concurrent.TimeUnit
+
+class QuickAnswerActivity : AppCompatActivity() {
+ private val mLiveSubscription = CompositeDisposable()
+ private val compositeDisposable = CompositeDisposable()
+ private val requestWritePermission = requestPermissionsAbs(
+ arrayOf(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ )
+ ) {
+ createCustomToast(this@QuickAnswerActivity).showToast(R.string.permission_all_granted_text)
+ }
+ private var etText: TextInputEditText? = null
+ private var notifier: TextingNotifier? = null
+ private var accountId = 0
+ private lateinit var msg: Message
+ private var messageIsRead = false
+ private var messagesRepository: IMessagesRepository = messages
+ override fun attachBaseContext(newBase: Context) {
+ super.attachBaseContext(Utils.updateActivityContext(newBase))
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ @StyleRes val theme: Int = when (Settings.get().main().themeOverlay) {
+ ThemeOverlay.AMOLED -> R.style.QuickReply_Amoled
+ ThemeOverlay.MD1 -> R.style.QuickReply_MD1
+ ThemeOverlay.OFF -> R.style.QuickReply
+ else -> R.style.QuickReply
+ }
+ setTheme(theme)
+ super.onCreate(savedInstanceState)
+ val focusToField = intent.getBooleanExtra(EXTRA_FOCUS_TO_FIELD, true)
+ if (!focusToField) {
+ window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
+ }
+ msg = (intent.extras?.getParcelableCompat(Extra.MESSAGE) ?: return)
+ accountId = (intent.extras ?: return).getInt(Extra.ACCOUNT_ID)
+ notifier = TextingNotifier(accountId)
+ setContentView(R.layout.activity_quick_answer)
+ val toolbar = findViewById(R.id.toolbar)
+ toolbar?.setNavigationIcon(R.drawable.arrow_left)
+ setSupportActionBar(toolbar)
+ val tvMessage = findViewById(R.id.item_message_text)
+ val tvTime = findViewById(R.id.item_message_time)
+ etText = findViewById(R.id.activity_quick_answer_edit_text)
+ val ivAvatar = findViewById(R.id.avatar)
+ val btnToDialog = findViewById(R.id.activity_quick_answer_to_dialog)
+ val btnSend = findViewById(R.id.activity_quick_answer_send)
+ val messageTime = AppTextUtils.getDateFromUnixTime(this, msg.date)
+ val title = intent.getStringExtra(Extra.TITLE)
+ supportActionBar?.title = title
+ tvMessage.setText(intent.getStringExtra(PARAM_BODY), TextView.BufferType.SPANNABLE)
+ tvTime.text = messageTime
+ val transformation = CurrentTheme.createTransformationForAvatar()
+ val imgUrl = intent.getStringExtra(Extra.IMAGE)
+ if (ivAvatar != null) {
+ ViewUtils.displayAvatar(ivAvatar, transformation, imgUrl, null)
+ }
+ val forwardMessagesRoot = findViewById(R.id.forward_messages)
+ val attachmentsRoot = findViewById(R.id.item_message_attachment_container)
+ val attachmentsHolder = AttachmentsHolder()
+ attachmentsHolder.setVgAudios(attachmentsRoot.findViewById(R.id.audio_attachments))
+ .setVgVideos(attachmentsRoot.findViewById(R.id.video_attachments))
+ .setVgDocs(attachmentsRoot.findViewById(R.id.docs_attachments))
+ .setVgArticles(attachmentsRoot.findViewById(R.id.articles_attachments))
+ .setVgPhotos(attachmentsRoot.findViewById(R.id.photo_attachments))
+ .setVgPosts(attachmentsRoot.findViewById(R.id.posts_attachments))
+ .setVoiceMessageRoot(attachmentsRoot.findViewById(R.id.voice_message_attachments))
+ val botKeyboardView = findViewById(R.id.input_keyboard_container)
+ if (botKeyboardView != null) {
+ val msgKeyboard = msg.keyboard
+ if (msgKeyboard != null && msgKeyboard.inline && msgKeyboard.buttons?.size.orZero() > 0) {
+ botKeyboardView.visibility = View.VISIBLE
+ botKeyboardView.setButtons(msgKeyboard.buttons, false)
+ } else {
+ botKeyboardView.visibility = View.GONE
+ }
+ botKeyboardView.setDelegate(object : BotKeyboardViewDelegate {
+ override fun didPressedButton(button: Keyboard.Button, needClose: Boolean) {
+ if (button.type == "open_link") {
+ LinkHelper.openLinkInBrowser(this@QuickAnswerActivity, button.link)
+ return
+ }
+ val builder = SaveMessageBuilder(accountId, msg.peerId)
+ .setPayload(button.payload).setBody(button.label)
+ compositeDisposable.add(
+ messagesRepository.put(builder)
+ .fromIOToMain()
+ .subscribe({ onMessageSaved() }) { throwable ->
+ onSavingError(
+ throwable
+ )
+ })
+ }
+ })
+ }
+ val hasAttachments =
+ msg.fwd.nonNullNoEmpty() || msg.attachments?.hasAttachments == true
+ attachmentsRoot.visibility = if (hasAttachments) View.VISIBLE else View.GONE
+ if (hasAttachments) {
+ val attachmentsViewBinder =
+ AttachmentsViewBinder(this, object : OnAttachmentsActionCallback {
+ override fun onPollOpen(poll: Poll) {}
+ override fun onVideoPlay(video: Video) {}
+ override fun onAudioPlay(position: Int, audios: ArrayList) {
+ startForPlayList(this@QuickAnswerActivity, audios, position, false)
+ }
+
+ override fun onForwardMessagesOpen(messages: ArrayList) {}
+ override fun onOpenOwner(ownerId: Int) {}
+ override fun onGoToMessagesLookup(message: Message) {}
+ override fun onDocPreviewOpen(document: Document) {}
+ override fun onPostOpen(post: Post) {}
+ override fun onLinkOpen(link: Link) {}
+ override fun onUrlOpen(url: String) {}
+ override fun onFaveArticle(article: Article) {}
+ override fun onShareArticle(article: Article) {}
+ override fun onWikiPageOpen(page: WikiPage) {}
+ override fun onPhotosOpen(
+ photos: ArrayList,
+ index: Int,
+ refresh: Boolean
+ ) {
+ }
+
+ override fun onUrlPhotoOpen(
+ url: String,
+ prefix: String,
+ photo_prefix: String
+ ) {
+ }
+
+ override fun onStoryOpen(story: Story) {}
+ override fun onWallReplyOpen(reply: WallReply) {}
+ override fun onAudioPlaylistOpen(playlist: AudioPlaylist) {}
+ override fun onPhotoAlbumOpen(album: PhotoAlbum) {}
+ override fun onMarketAlbumOpen(market_album: MarketAlbum) {}
+ override fun onMarketOpen(market: Market) {}
+ override fun onArtistOpen(artist: AudioArtist) {}
+ override fun onRequestWritePermissions() {
+ requestWritePermission.launch()
+ }
+ })
+ attachmentsViewBinder.setVoiceActionListener(object : VoiceActionListener {
+ override fun onVoiceHolderBinded(voiceMessageId: Int, voiceHolderId: Int) {}
+ override fun onVoicePlayButtonClick(
+ voiceHolderId: Int,
+ voiceMessageId: Int,
+ messageId: Int,
+ peerId: Int,
+ voiceMessage: VoiceMessage
+ ) {
+ val audio =
+ Audio().setId(voiceMessage.getId()).setOwnerId(voiceMessage.getOwnerId())
+ .setTitle(
+ voiceMessage.getId().toString() + "_" + voiceMessage.getOwnerId()
+ )
+ .setArtist("Voice")
+ .setIsLocal().setDuration(voiceMessage.getDuration()).setUrl(
+ Utils.firstNonEmptyString(
+ voiceMessage.getLinkMp3(),
+ voiceMessage.getLinkOgg()
+ )
+ )
+ startForPlayList(this@QuickAnswerActivity, ArrayList(listOf(audio)), 0, false)
+ }
+
+ override fun onVoiceTogglePlaybackSpeed() {}
+ override fun onTranscript(voiceMessageId: String, messageId: Int) {}
+ })
+ attachmentsViewBinder.displayAttachments(
+ msg.attachments,
+ attachmentsHolder,
+ true,
+ msg.getObjectId(),
+ msg.peerId
+ )
+ attachmentsViewBinder.displayForwards(msg.fwd, forwardMessagesRoot, true)
+ }
+ etText?.addTextChangedListener(object : TextWatcherAdapter() {
+ override fun afterTextChanged(s: Editable?) {
+ if (!messageIsRead) {
+ setMessageAsRead()
+ messageIsRead = true
+ }
+ cancelFinishWithDelay()
+ notifier?.notifyAboutTyping(msg.peerId)
+ }
+ })
+ btnSend.setOnClickListener { send() }
+ btnToDialog.setOnClickListener {
+ val intent = Intent(this, MainActivity::class.java)
+ intent.action = MainActivity.ACTION_OPEN_PLACE
+ val chatPlace = PlaceFactory.getChatPlace(
+ accountId, accountId, Peer(
+ msg.peerId
+ ).setAvaUrl(imgUrl).setTitle(title)
+ )
+ intent.putExtra(Extra.PLACE, chatPlace)
+ startActivity(intent)
+ finish()
+ }
+ val liveDelay = intent.getBooleanExtra(EXTRA_LIVE_DELAY, false)
+ if (liveDelay) {
+ finishWithDelay()
+ }
+ }
+
+ private fun finishWithDelay() {
+ mLiveSubscription.add(
+ Observable.just(Any())
+ .delay(1, TimeUnit.MINUTES)
+ .subscribe { finish() })
+ }
+
+ internal fun cancelFinishWithDelay() {
+ mLiveSubscription.dispose()
+ }
+
+ override fun onDestroy() {
+ mLiveSubscription.dispose()
+ compositeDisposable.dispose()
+ super.onDestroy()
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ finish()
+ return true
+ }
+
+ /**
+ * Отправка сообщения
+ */
+ private fun send() {
+ val trimmed_text = etText?.text.toString().trim { it <= ' ' }
+ if (trimmed_text.isEmpty()) {
+ createCustomToast(this).setDuration(Toast.LENGTH_LONG)
+ .showToastError(R.string.text_hint)
+ return
+ }
+ val requireEncryption = Settings.get()
+ .security()
+ .isMessageEncryptionEnabled(accountId, msg.peerId)
+ @KeyLocationPolicy var policy = KeyLocationPolicy.PERSIST
+ if (requireEncryption) {
+ policy = Settings.get()
+ .security()
+ .getEncryptionLocationPolicy(accountId, msg.peerId)
+ }
+ val builder = SaveMessageBuilder(accountId, msg.peerId)
+ .setBody(trimmed_text)
+ .setForwardMessages(ArrayList(setOf(msg)))
+ .setRequireEncryption(requireEncryption)
+ .setKeyLocationPolicy(policy)
+ compositeDisposable.add(
+ messagesRepository.put(builder)
+ .fromIOToMain()
+ .subscribe({ onMessageSaved() }) { throwable ->
+ onSavingError(
+ throwable
+ )
+ })
+ }
+
+ internal fun onSavingError(throwable: Throwable) {
+ createCustomToast(this).showToastThrowable(throwable)
+ }
+
+ internal fun onMessageSaved() {
+ NotificationHelper.tryCancelNotificationForPeer(this, accountId, msg.peerId)
+ messagesRepository.runSendingQueue()
+ finish()
+ }
+
+ internal fun setMessageAsRead() {
+ compositeDisposable.add(
+ messagesRepository.markAsRead(accountId, msg.peerId, msg.getObjectId())
+ .fromIOToMain()
+ .subscribe(RxUtils.dummy(), RxUtils.ignore())
+ )
+ }
+
+ companion object {
+ const val PARAM_BODY = "body"
+ const val EXTRA_FOCUS_TO_FIELD = "focus_to_field"
+ const val EXTRA_LIVE_DELAY = "live_delay"
+
+
+ fun forStart(
+ context: Context?,
+ accountId: Int,
+ msg: Message?,
+ body: String?,
+ imgUrl: String?,
+ title: String?
+ ): Intent {
+ val intent = Intent(context, QuickAnswerActivity::class.java)
+ intent.putExtra(PARAM_BODY, body)
+ intent.putExtra(Extra.ACCOUNT_ID, accountId)
+ intent.putExtra(Extra.MESSAGE, msg)
+ intent.putExtra(Extra.TITLE, title)
+ intent.putExtra(Extra.IMAGE, imgUrl)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ return intent
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/SelectionUtils.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/SelectionUtils.kt
new file mode 100644
index 000000000..fe831da83
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/SelectionUtils.kt
@@ -0,0 +1,81 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.model.FavePage
+import dev.ragnarok.fenrir.model.Owner
+import dev.ragnarok.fenrir.model.SelectProfileCriteria
+import dev.ragnarok.fenrir.model.User
+import dev.ragnarok.fenrir.util.Logger
+import dev.ragnarok.fenrir.util.Utils
+
+object SelectionUtils {
+ private val TAG = SelectionUtils::class.java.simpleName
+ private const val VIEW_TAG = "SelectionUtils.SelectionView"
+
+
+ fun addSelectionProfileSupport(context: Context?, root: ViewGroup?, mayBeUser: Any) {
+ if (context !is ProfileSelectable || root == null) return
+ val criteria = (context as ProfileSelectable).acceptableCriteria
+ var canSelect =
+ if (criteria?.getIsPeopleOnly() == true) mayBeUser is User else mayBeUser is Owner || mayBeUser is FavePage
+ if (canSelect && criteria?.getOwnerType() == SelectProfileCriteria.OwnerType.ONLY_FRIENDS) {
+ assert(mayBeUser is User)
+ canSelect = (mayBeUser as User).isFriend
+ }
+ val callack = context as ProfileSelectable
+ var selectionView =
+ root.findViewWithTag(VIEW_TAG)
+ if (!canSelect && selectionView == null) return
+ if (canSelect && selectionView == null) {
+ selectionView = ImageView(context)
+ selectionView.setImageResource(R.drawable.plus)
+ selectionView.tag = VIEW_TAG
+ selectionView.setBackgroundResource(R.drawable.circle_back)
+ selectionView.background.alpha = 150
+ selectionView.layoutParams = createLayoutParams(root)
+ val dp4px = Utils.dpToPx(4f, context).toInt()
+ selectionView.setPadding(dp4px, dp4px, dp4px, dp4px)
+ Logger.d(TAG, "Added new selectionView")
+ root.addView(selectionView)
+ } else {
+ Logger.d(TAG, "Re-use selectionView")
+ }
+ selectionView?.visibility = if (canSelect) View.VISIBLE else View.GONE
+ if (!canSelect) {
+ selectionView.setOnClickListener(null)
+ } else {
+ if (mayBeUser is FavePage && mayBeUser.owner != null) {
+ mayBeUser.owner?.let { vv ->
+ selectionView.setOnClickListener { callack.select(vv) }
+ }
+ } else if (mayBeUser is Owner) {
+ selectionView.setOnClickListener {
+ callack.select(
+ mayBeUser
+ )
+ }
+ }
+ }
+ }
+
+ private fun createLayoutParams(parent: ViewGroup): ViewGroup.LayoutParams {
+ return if (parent is FrameLayout) {
+ val margin = Utils.dpToPx(6f, parent.getContext()).toInt()
+ val params = FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ params.bottomMargin = margin
+ params.leftMargin = margin
+ params.rightMargin = margin
+ params.topMargin = margin
+ params
+ } else {
+ throw IllegalArgumentException("Not yet impl for parent: " + parent.javaClass.simpleName)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/SendAttachmentsActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/SendAttachmentsActivity.kt
new file mode 100644
index 000000000..ca5913fd3
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/SendAttachmentsActivity.kt
@@ -0,0 +1,80 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.model.AbsModel
+import dev.ragnarok.fenrir.model.ModelsBundle
+import dev.ragnarok.fenrir.model.Peer
+import dev.ragnarok.fenrir.place.PlaceFactory
+import dev.ragnarok.fenrir.util.MainActivityTransforms
+import dev.ragnarok.fenrir.util.ViewUtils
+
+/**
+ * Тот же MainActivity, предназначенный для шаринга контента
+ * Отличие только в том, что этот активити может существовать в нескольких экземплярах
+ */
+class SendAttachmentsActivity : MainActivity() {
+ @MainActivityTransforms
+ override fun getMainActivityTransform(): Int {
+ return MainActivityTransforms.SEND_ATTACHMENTS
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // потому, что в onBackPressed к этому числу будут прибавлять 2000 !!!! и выход за границы
+ mLastBackPressedTime = Long.MAX_VALUE - DOUBLE_BACK_PRESSED_TIMEOUT
+ }
+
+ public override fun onDestroy() {
+ ViewUtils.keyboardHide(this)
+ super.onDestroy()
+ }
+
+ companion object {
+ fun startForSendAttachments(context: Context, accountId: Int, bundle: ModelsBundle?) {
+ val intent = Intent(context, SendAttachmentsActivity::class.java)
+ intent.action = ACTION_SEND_ATTACHMENTS
+ intent.putExtra(EXTRA_INPUT_ATTACHMENTS, bundle)
+ intent.putExtra(EXTRA_NO_REQUIRE_PIN, true)
+ intent.putExtra(Extra.PLACE, PlaceFactory.getDialogsPlace(accountId, accountId, null))
+ context.startActivity(intent)
+ }
+
+ fun startForSendAttachmentsFor(
+ context: Context,
+ accountId: Int,
+ peer: Peer,
+ bundle: ModelsBundle?
+ ) {
+ val intent = Intent(context, ChatActivity::class.java)
+ intent.action = ChatActivity.ACTION_OPEN_PLACE
+ intent.putExtra(EXTRA_INPUT_ATTACHMENTS, bundle)
+ intent.putExtra(Extra.PLACE, PlaceFactory.getChatPlace(accountId, accountId, peer))
+ context.startActivity(intent)
+ }
+
+ fun startForSendLink(context: Context, link: String?) {
+ val intent = Intent(context, SendAttachmentsActivity::class.java)
+ intent.action = Intent.ACTION_SEND
+ intent.putExtra(Intent.EXTRA_TEXT, link)
+ context.startActivity(intent)
+ }
+
+
+ fun startForSendAttachments(context: Context, accountId: Int, model: AbsModel) {
+ startForSendAttachments(context, accountId, ModelsBundle(1).append(model))
+ }
+
+
+ fun startForSendAttachmentsFor(
+ context: Context,
+ accountId: Int,
+ peer: Peer,
+ model: AbsModel
+ ) {
+ startForSendAttachmentsFor(context, accountId, peer, ModelsBundle(1).append(model))
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/SinglePhotoActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/SinglePhotoActivity.kt
new file mode 100644
index 000000000..86b89d66f
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/SinglePhotoActivity.kt
@@ -0,0 +1,426 @@
+package dev.ragnarok.fenrir.activity
+
+import android.Manifest
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
+import android.widget.RelativeLayout
+import androidx.annotation.ColorInt
+import androidx.annotation.IdRes
+import androidx.annotation.LayoutRes
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.squareup.picasso3.Callback
+import dev.ragnarok.fenrir.*
+import dev.ragnarok.fenrir.activity.slidr.Slidr.attach
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrListener
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrPosition
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment
+import dev.ragnarok.fenrir.listener.AppStyleable
+import dev.ragnarok.fenrir.picasso.PicassoInstance
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceProvider
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.util.AppPerms
+import dev.ragnarok.fenrir.util.AppPerms.requestPermissionsAbs
+import dev.ragnarok.fenrir.util.DownloadWorkUtils
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.rxutils.RxUtils
+import dev.ragnarok.fenrir.util.toast.CustomToast
+import dev.ragnarok.fenrir.view.CircleCounterButton
+import dev.ragnarok.fenrir.view.TouchImageView
+import dev.ragnarok.fenrir.view.natives.rlottie.RLottieImageView
+import dev.ragnarok.fenrir.view.pager.WeakPicassoLoadCallback
+import io.reactivex.rxjava3.core.Completable
+import io.reactivex.rxjava3.disposables.Disposable
+import java.io.File
+import java.lang.ref.WeakReference
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+class SinglePhotoActivity : NoMainActivity(), PlaceProvider, AppStyleable {
+ private var url: String? = null
+ private var prefix: String? = null
+ private var photo_prefix: String? = null
+ private var mFullscreen = false
+ private var mDownload: CircleCounterButton? = null
+
+ @LayoutRes
+ override fun getNoMainContentView(): Int {
+ return R.layout.fragment_single_url_photo
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ handleIntent(intent)
+ mFullscreen = savedInstanceState?.getBoolean("mFullscreen") == true
+
+ url?.let {
+ mDownload?.visibility =
+ if (it.contains("content://") || it.contains("file://")) View.GONE else View.VISIBLE
+ }
+ url ?: run {
+ mDownload?.visibility = View.GONE
+ }
+ val ret = PhotoViewHolder(this)
+ ret.bindTo(url)
+ mDownload = findViewById(R.id.button_download)
+ val mContentRoot = findViewById(R.id.photo_single_root)
+ attach(
+ this,
+ SlidrConfig.Builder().setAlphaForView(false).fromUnColoredToColoredStatusBar(true)
+ .position(SlidrPosition.VERTICAL)
+ .listener(object : SlidrListener {
+ override fun onSlideStateChanged(state: Int) {
+
+ }
+
+ @SuppressLint("Range")
+ override fun onSlideChange(percent: Float) {
+ var tmp = 1f - percent
+ tmp *= 4
+ tmp = Utils.clamp(1f - tmp, 0f, 1f)
+ if (Utils.hasOreo()) {
+ mContentRoot?.setBackgroundColor(Color.argb(tmp, 0f, 0f, 0f))
+ } else {
+ mContentRoot?.setBackgroundColor(
+ Color.argb(
+ (tmp * 255).toInt(),
+ 0,
+ 0,
+ 0
+ )
+ )
+ }
+ mDownload?.alpha = tmp
+ ret.photo.alpha = Utils.clamp(percent, 0f, 1f)
+ }
+
+ override fun onSlideOpened() {
+ }
+
+ override fun onSlideClosed(): Boolean {
+ finish()
+ overridePendingTransition(0, 0)
+ return true
+ }
+
+ }).build()
+ )
+
+ ret.photo.setOnLongClickListener {
+ doSaveOnDrive(true)
+ true
+ }
+ mDownload?.setOnClickListener { doSaveOnDrive(true) }
+ resolveFullscreenViews()
+
+ ret.photo.setOnTouchListener { view: View, event: MotionEvent ->
+ if (event.pointerCount >= 2 || view.canScrollHorizontally(1) && view.canScrollHorizontally(
+ -1
+ )
+ ) {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
+ mContentRoot?.requestDisallowInterceptTouchEvent(true)
+ return@setOnTouchListener false
+ }
+ MotionEvent.ACTION_UP -> {
+ mContentRoot?.requestDisallowInterceptTouchEvent(false)
+ return@setOnTouchListener true
+ }
+ }
+ }
+ true
+ }
+ }
+
+ private val requestWritePermission = requestPermissionsAbs(
+ arrayOf(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ )
+ ) {
+ doSaveOnDrive(false)
+ }
+
+ private fun handleIntent(intent: Intent?) {
+ if (intent == null) {
+ finish()
+ return
+ }
+ if (Intent.ACTION_VIEW == intent.action) {
+ val data = intent.data
+ url = "full_" + data.toString()
+ prefix = "tmp"
+ photo_prefix = "tmp"
+ } else {
+ url = intent.extras?.getString(Extra.URL)
+ prefix = DownloadWorkUtils.makeLegalFilenameFromArg(
+ intent.extras?.getString(Extra.STATUS),
+ null
+ )
+ photo_prefix = DownloadWorkUtils.makeLegalFilenameFromArg(
+ intent.extras?.getString(Extra.KEY),
+ null
+ )
+ }
+ }
+
+ override fun openPlace(place: Place) {
+ val args = place.safeArguments()
+ when (place.type) {
+ Place.PLAYER -> {
+ val player = supportFragmentManager.findFragmentByTag("audio_player")
+ if (player is AudioPlayerFragment) player.dismiss()
+ AudioPlayerFragment.newInstance(args).show(supportFragmentManager, "audio_player")
+ }
+ else -> Utils.openPlaceWithSwipebleActivity(this, place)
+ }
+ }
+
+ private fun doSaveOnDrive(Request: Boolean) {
+ if (Request) {
+ if (!AppPerms.hasReadWriteStoragePermission(App.instance)) {
+ requestWritePermission.launch()
+ }
+ }
+ var dir = File(Settings.get().other().photoDir)
+ if (!dir.isDirectory) {
+ val created = dir.mkdirs()
+ if (!created) {
+ CustomToast.createCustomToast(this).showToastError("Can't create directory $dir")
+ return
+ }
+ } else dir.setLastModified(Calendar.getInstance().time.time)
+ if (prefix != null && Settings.get().other().isPhoto_to_user_dir) {
+ val dir_final = File(dir.absolutePath + "/" + prefix)
+ if (!dir_final.isDirectory) {
+ val created = dir_final.mkdirs()
+ if (!created) {
+ CustomToast.createCustomToast(this)
+ .showToastError("Can't create directory $dir")
+ return
+ }
+ } else dir_final.setLastModified(Calendar.getInstance().time.time)
+ dir = dir_final
+ }
+ val DOWNLOAD_DATE_FORMAT: DateFormat =
+ SimpleDateFormat("yyyyMMdd_HHmmss", Utils.appLocale)
+ url?.let {
+ DownloadWorkUtils.doDownloadPhoto(
+ this,
+ it,
+ dir.absolutePath,
+ Utils.firstNonEmptyString(prefix, "null") + "." + Utils.firstNonEmptyString(
+ photo_prefix,
+ "null"
+ ) + ".profile." + DOWNLOAD_DATE_FORMAT.format(Date())
+ )
+ }
+ }
+
+ private inner class PhotoViewHolder(view: SinglePhotoActivity) : Callback {
+ private val ref = WeakReference(view)
+ val reload: FloatingActionButton
+ private val mPicassoLoadCallback: WeakPicassoLoadCallback
+ val photo: TouchImageView
+ val progress: RLottieImageView
+ var animationDispose: Disposable = Disposable.disposed()
+ private var mAnimationLoaded = false
+ private var mLoadingNow = false
+ fun bindTo(url: String?) {
+ reload.setOnClickListener {
+ reload.visibility = View.INVISIBLE
+ if (url.nonNullNoEmpty()) {
+ loadImage(url)
+ } else PicassoInstance.with().cancelRequest(photo)
+ }
+ if (url.nonNullNoEmpty()) {
+ loadImage(url)
+ } else {
+ PicassoInstance.with().cancelRequest(photo)
+ CustomToast.createCustomToast(ref.get()).showToast(R.string.empty_url)
+ }
+ }
+
+ private fun resolveProgressVisibility(forceStop: Boolean) {
+ animationDispose.dispose()
+ if (mAnimationLoaded && !mLoadingNow && !forceStop) {
+ mAnimationLoaded = false
+ val k = ObjectAnimator.ofFloat(progress, View.ALPHA, 0.0f).setDuration(1000)
+ k.addListener(object : StubAnimatorListener() {
+ override fun onAnimationEnd(animation: Animator) {
+ progress.clearAnimationDrawable()
+ progress.visibility = View.GONE
+ progress.alpha = 1f
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ progress.clearAnimationDrawable()
+ progress.visibility = View.GONE
+ progress.alpha = 1f
+ }
+ })
+ k.start()
+ } else if (mAnimationLoaded && !mLoadingNow) {
+ mAnimationLoaded = false
+ progress.clearAnimationDrawable()
+ progress.visibility = View.GONE
+ } else if (mLoadingNow) {
+ animationDispose = Completable.create {
+ it.onComplete()
+ }.delay(300, TimeUnit.MILLISECONDS).fromIOToMain().subscribe({
+ mAnimationLoaded = true
+ progress.visibility = View.VISIBLE
+ progress.fromRes(
+ dev.ragnarok.fenrir_common.R.raw.loading,
+ Utils.dp(100F),
+ Utils.dp(100F),
+ intArrayOf(
+ 0x000000,
+ CurrentTheme.getColorPrimary(ref.get()),
+ 0x777777,
+ CurrentTheme.getColorSecondary(ref.get())
+ )
+ )
+ progress.playAnimation()
+ }, RxUtils.ignore())
+ }
+ }
+
+ private fun loadImage(url: String?) {
+ mLoadingNow = true
+ resolveProgressVisibility(true)
+ PicassoInstance.with()
+ .load(url)
+ .into(photo, mPicassoLoadCallback)
+ }
+
+ @IdRes
+ private fun idOfImageView(): Int {
+ return R.id.image_view
+ }
+
+ @IdRes
+ private fun idOfProgressBar(): Int {
+ return R.id.progress_bar
+ }
+
+ override fun onSuccess() {
+ mLoadingNow = false
+ resolveProgressVisibility(false)
+ reload.visibility = View.INVISIBLE
+ }
+
+ override fun onError(t: Throwable) {
+ mLoadingNow = false
+ resolveProgressVisibility(true)
+ reload.visibility = View.VISIBLE
+ }
+
+ init {
+ photo = view.findViewById(idOfImageView())
+ photo.maxZoom = 8f
+ photo.doubleTapScale = 2f
+ photo.doubleTapMaxZoom = 4f
+ progress = view.findViewById(idOfProgressBar())
+ reload = view.findViewById(R.id.goto_button)
+ mPicassoLoadCallback = WeakPicassoLoadCallback(this)
+ photo.setOnClickListener { toggleFullscreen() }
+ }
+ }
+
+ internal fun toggleFullscreen() {
+ mFullscreen = !mFullscreen
+ resolveFullscreenViews()
+ }
+
+ private fun resolveFullscreenViews() {
+ mDownload?.visibility = if (mFullscreen) View.GONE else View.VISIBLE
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putBoolean("mFullscreen", mFullscreen)
+ }
+
+ override fun hideMenu(hide: Boolean) {}
+ override fun openMenu(open: Boolean) {}
+
+ @Suppress("DEPRECATION")
+ override fun setStatusbarColored(colored: Boolean, invertIcons: Boolean) {
+ val statusbarNonColored = CurrentTheme.getStatusBarNonColored(this)
+ val statusbarColored = CurrentTheme.getStatusBarColor(this)
+ val w = window
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.statusBarColor = if (colored) statusbarColored else statusbarNonColored
+ @ColorInt val navigationColor =
+ if (colored) CurrentTheme.getNavigationBarColor(this) else Color.BLACK
+ w.navigationBarColor = navigationColor
+ if (Utils.hasMarshmallow()) {
+ var flags = window.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ }
+ window.decorView.systemUiVisibility = flags
+ }
+ if (Utils.hasOreo()) {
+ var flags = window.decorView.systemUiVisibility
+ if (invertIcons) {
+ flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ w.decorView.systemUiVisibility = flags
+ w.navigationBarColor = Color.WHITE
+ } else {
+ flags = flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ w.decorView.systemUiVisibility = flags
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ ActivityFeatures.Builder()
+ .begin()
+ .setHideNavigationMenu(true)
+ .setBarsColored(colored = false, invertIcons = false)
+ .build()
+ .apply(this)
+ }
+
+ companion object {
+ private const val ACTION_OPEN =
+ "dev.ragnarok.fenrir.activity.SinglePhotoActivity"
+
+ fun newInstance(context: Context, args: Bundle?): Intent {
+ val ph = Intent(context, SinglePhotoActivity::class.java)
+ val targetArgs = Bundle()
+ targetArgs.putAll(args)
+ ph.action = ACTION_OPEN
+ ph.putExtras(targetArgs)
+ return ph
+ }
+
+ fun buildArgs(url: String?, download_prefix: String?, photo_prefix: String?): Bundle {
+ val args = Bundle()
+ args.putString(Extra.URL, url)
+ args.putString(Extra.STATUS, download_prefix)
+ args.putString(Extra.KEY, photo_prefix)
+ return args
+ }
+ }
+}
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/SwipebleActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/SwipebleActivity.kt
new file mode 100644
index 000000000..9107ec72a
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/SwipebleActivity.kt
@@ -0,0 +1,49 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import dev.ragnarok.fenrir.activity.slidr.Slidr.attach
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.util.MainActivityTransforms
+import dev.ragnarok.fenrir.util.ViewUtils
+
+/**
+ * Тот же MainActivity, предназначенный для шаринга контента
+ * Отличие только в том, что этот активити может существовать в нескольких экземплярах
+ */
+class SwipebleActivity : MainActivity() {
+ @MainActivityTransforms
+ override fun getMainActivityTransform(): Int {
+ return MainActivityTransforms.SWIPEBLE
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ attach(
+ this,
+ SlidrConfig.Builder().scrimColor(CurrentTheme.getColorBackground(this)).build()
+ )
+ // потому, что в onBackPressed к этому числу будут прибавлять 2000 !!!! и выход за границы
+ mLastBackPressedTime = Long.MAX_VALUE - DOUBLE_BACK_PRESSED_TIMEOUT
+ }
+
+ public override fun onDestroy() {
+ ViewUtils.keyboardHide(this)
+ super.onDestroy()
+ }
+
+ companion object {
+
+ fun applyIntent(intent: Intent) {
+ intent.putExtra(EXTRA_NO_REQUIRE_PIN, true)
+ }
+
+
+ fun start(context: Context, intent: Intent) {
+ intent.putExtra(EXTRA_NO_REQUIRE_PIN, true)
+ context.startActivity(intent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ValidateActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ValidateActivity.kt
new file mode 100644
index 000000000..59aa6f1c3
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/ValidateActivity.kt
@@ -0,0 +1,175 @@
+package dev.ragnarok.fenrir.activity
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.os.Bundle
+import android.util.Log
+import android.webkit.CookieManager
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.activity.OnBackPressedCallback
+import androidx.appcompat.app.AppCompatActivity
+import dev.ragnarok.fenrir.Constants
+import dev.ragnarok.fenrir.Includes
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.slidr.Slidr.attach
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrListener
+import dev.ragnarok.fenrir.api.Auth
+import dev.ragnarok.fenrir.api.IValidateProvider
+import dev.ragnarok.fenrir.api.util.VKStringUtils
+import dev.ragnarok.fenrir.nonNullNoEmpty
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.settings.theme.ThemesController.currentStyle
+import dev.ragnarok.fenrir.util.Logger
+import dev.ragnarok.fenrir.util.rxutils.RxUtils
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+
+class ValidateActivity : AppCompatActivity() {
+ private var validateProvider: IValidateProvider? = null
+ private val mCompositeDisposable = CompositeDisposable()
+ private var urlVal: String? = null
+
+ @SuppressLint("SetJavaScriptEnabled")
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ setTheme(currentStyle())
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_login)
+
+ urlVal = (intent.getStringExtra(EXTRA_VALIDATE) ?: return)
+
+ validateProvider = Includes.validationProvider
+
+ validateProvider?.let {
+ mCompositeDisposable.add(
+ it.observeWaiting()
+ .filter { ob -> ob == urlVal }
+ .observeOn(Includes.provideMainThreadScheduler())
+ .subscribe({ onWaitingRequestReceived() }, RxUtils.ignore())
+ )
+ mCompositeDisposable.add(
+ it.observeCanceling()
+ .filter { ob -> ob == urlVal }
+ .observeOn(Includes.provideMainThreadScheduler())
+ .subscribe({ onRequestCancelled() }, RxUtils.ignore())
+ )
+ }
+
+ attach(
+ this,
+ SlidrConfig.Builder().listener(object : SlidrListener {
+ override fun onSlideStateChanged(state: Int) {
+
+ }
+
+ override fun onSlideChange(percent: Float) {
+
+ }
+
+ override fun onSlideOpened() {
+
+ }
+
+ override fun onSlideClosed(): Boolean {
+ cancel()
+ return true
+ }
+ }).scrimColor(CurrentTheme.getColorBackground(this)).build()
+ )
+ val webview = findViewById(R.id.vkontakteview)
+ webview.settings.javaScriptEnabled = true
+ webview.clearCache(true)
+ webview.settings.userAgentString = Constants.USER_AGENT_ACCOUNT
+
+ //Чтобы получать уведомления об окончании загрузки страницы
+ webview.webViewClient = VkontakteWebViewClient()
+ val cookieManager = CookieManager.getInstance()
+ cookieManager.removeAllCookies { aBoolean: Boolean ->
+ Log.d(
+ TAG,
+ "Cookie removed: $aBoolean"
+ )
+ }
+ webview.loadUrl(urlVal ?: "")
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ }
+ })
+ }
+
+ internal fun cancel() {
+ urlVal?.let { validateProvider?.cancel(it) }
+ finish()
+ }
+
+ private fun onRequestCancelled() {
+ finish()
+ }
+
+ private fun onWaitingRequestReceived() {
+ urlVal?.let { validateProvider?.notifyThatValidateEntryActive(it) }
+ }
+
+ override fun onDestroy() {
+ mCompositeDisposable.dispose()
+ super.onDestroy()
+ }
+
+ internal fun parseUrl(url: String?) {
+ try {
+ if (url == null) {
+ return
+ }
+ Logger.d(TAG, "url=$url")
+ if (url.startsWith(Auth.redirect_url)) {
+ if (!url.contains("error=")) {
+ val intent = Intent()
+ try {
+ val accessToken = tryExtractAccessToken(url)
+ val userId = tryExtractUserId(url)
+ if (accessToken.nonNullNoEmpty() || userId.nonNullNoEmpty()) {
+ userId?.toInt()
+ ?.let {
+ Settings.get().accounts().storeAccessToken(it, accessToken)
+ }
+ }
+ } catch (ignored: Exception) {
+ }
+ setResult(RESULT_OK, intent)
+ }
+ urlVal?.let { validateProvider?.enterState(it, true) }
+ finish()
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ private inner class VkontakteWebViewClient : WebViewClient() {
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+ parseUrl(url)
+ }
+ }
+
+ companion object {
+ private val TAG = ValidateActivity::class.java.simpleName
+ private const val EXTRA_VALIDATE = "validate"
+
+
+ fun createIntent(context: Context, validate_url: String?): Intent {
+ return Intent(context, ValidateActivity::class.java)
+ .putExtra(EXTRA_VALIDATE, validate_url)
+ }
+
+ internal fun tryExtractAccessToken(url: String): String? {
+ return VKStringUtils.extractPattern(url, "access_token=(.*?)&")
+ }
+
+ internal fun tryExtractUserId(url: String): String? {
+ return VKStringUtils.extractPattern(url, "user_id=(\\d*)")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/VideoPlayerActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/VideoPlayerActivity.kt
new file mode 100644
index 000000000..4915db84f
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/VideoPlayerActivity.kt
@@ -0,0 +1,563 @@
+package dev.ragnarok.fenrir.activity
+
+import android.annotation.SuppressLint
+import android.app.AppOpsManager
+import android.app.PictureInPictureParams
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.res.Configuration
+import android.graphics.Color
+import android.os.Build
+import android.os.Bundle
+import android.os.Process
+import android.util.Rational
+import android.view.SurfaceHolder
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.ImageView
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.ColorInt
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import androidx.customview.widget.ViewDragHelper.STATE_IDLE
+import dev.ragnarok.fenrir.*
+import dev.ragnarok.fenrir.Includes.proxySettings
+import dev.ragnarok.fenrir.activity.SwipebleActivity.Companion.applyIntent
+import dev.ragnarok.fenrir.activity.slidr.Slidr.attach
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrListener
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrPosition
+import dev.ragnarok.fenrir.listener.AppStyleable
+import dev.ragnarok.fenrir.media.video.ExoVideoPlayer
+import dev.ragnarok.fenrir.media.video.IVideoPlayer
+import dev.ragnarok.fenrir.model.Commented
+import dev.ragnarok.fenrir.model.InternalVideoSize
+import dev.ragnarok.fenrir.model.Video
+import dev.ragnarok.fenrir.model.VideoSize
+import dev.ragnarok.fenrir.place.PlaceFactory
+import dev.ragnarok.fenrir.push.OwnerInfo
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.settings.theme.ThemesController.currentStyle
+import dev.ragnarok.fenrir.util.Logger
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.toast.CustomToast
+import dev.ragnarok.fenrir.view.ExpandableSurfaceView
+import dev.ragnarok.fenrir.view.VideoControllerView
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+
+class VideoPlayerActivity : AppCompatActivity(), SurfaceHolder.Callback,
+ VideoControllerView.MediaPlayerControl, IVideoPlayer.IVideoSizeChangeListener, AppStyleable {
+ private val mCompositeDisposable = CompositeDisposable()
+ private var mDecorView: View? = null
+ private var mSpeed: ImageView? = null
+ private var mControllerView: VideoControllerView? = null
+ private var mSurfaceView: ExpandableSurfaceView? = null
+ private var mPlayer: IVideoPlayer? = null
+ private var video: Video? = null
+ private var onStopCalled = false
+ private var seekSave: Long = -1
+
+ @InternalVideoSize
+ private var size = 0
+ private var doNotPause = false
+ private val requestSwipeble = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { doNotPause = false }
+ private var isLocal = false
+ private var isLandscape = false
+ private fun onOpen() {
+ val intent = Intent(this, SwipebleActivity::class.java)
+ intent.action = MainActivity.ACTION_OPEN_WALL
+ intent.putExtra(Extra.OWNER_ID, (video ?: return).ownerId)
+ doNotPause = true
+ applyIntent(intent)
+ requestSwipeble.launch(intent)
+ }
+
+ override fun onStop() {
+ onStopCalled = true
+ super.onStop()
+ }
+
+ override fun finish() {
+ finishAndRemoveTask()
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ Logger.d(VideoPlayerActivity::class.java.name, "onNewIntent, intent: $intent")
+ handleIntent(intent, true)
+ }
+
+ private fun handleIntent(intent: Intent?, update: Boolean) {
+ if (intent == null) {
+ return
+ }
+ video = intent.getParcelableExtraCompat(EXTRA_VIDEO)
+ size = intent.getIntExtra(EXTRA_SIZE, InternalVideoSize.SIZE_240)
+ isLocal = intent.getBooleanExtra(EXTRA_LOCAL, false)
+ val actionBar = supportActionBar
+ actionBar?.title = video?.title
+ actionBar?.subtitle = video?.description
+ if (!isLocal && video != null) {
+ mCompositeDisposable.add(OwnerInfo.getRx(
+ this,
+ Settings.get().accounts().current,
+ (video ?: return).ownerId
+ )
+ .fromIOToMain()
+ .subscribe({ userInfo ->
+ val av =
+ findViewById(R.id.toolbar_avatar)
+ av.setImageBitmap(userInfo.avatar)
+ av.setOnClickListener { onOpen() }
+ if (video?.description.isNullOrEmpty() && actionBar != null) {
+ actionBar.subtitle = userInfo.owner.fullName
+ }
+ }) { })
+ } else {
+ findViewById(R.id.toolbar_avatar).visibility = View.GONE
+ }
+ if (update) {
+ val settings = proxySettings
+ val config = settings.activeProxy
+ val url = fileUrl
+ mPlayer?.updateSource(this, url, config, size)
+ mPlayer?.play()
+ mControllerView?.updateComment(!isLocal && video?.isCanComment == true)
+ }
+ }
+
+ override fun attachBaseContext(newBase: Context) {
+ super.attachBaseContext(Utils.updateActivityContext(newBase))
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putInt("size", size)
+ outState.putParcelable("video", video)
+ outState.putBoolean("isLocal", isLocal)
+ outState.putLong("seek", mPlayer?.currentPosition ?: -1)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ setTheme(currentStyle())
+ Utils.prepareDensity(this)
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_video)
+ val surfaceContainer = findViewById(R.id.videoSurfaceContainer)
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ mDecorView = window.decorView
+ val toolbar = findViewById(R.id.toolbar)
+ setSupportActionBar(toolbar)
+ if (toolbar != null) {
+ toolbar.setNavigationIcon(R.drawable.arrow_left)
+ toolbar.setNavigationOnClickListener { finish() }
+ }
+ if (savedInstanceState == null) {
+ handleIntent(intent, false)
+ } else {
+ size = savedInstanceState.getInt("size")
+ video = savedInstanceState.getParcelableCompat("video")
+ isLocal = savedInstanceState.getBoolean("isLocal")
+ seekSave = savedInstanceState.getLong("seek")
+ val actionBar = supportActionBar
+ actionBar?.title = video?.title
+ actionBar?.subtitle = video?.description
+ if (!isLocal && video != null) {
+ mCompositeDisposable.add(OwnerInfo.getRx(
+ this,
+ Settings.get().accounts().current,
+ (video ?: return).ownerId
+ )
+ .fromIOToMain()
+ .subscribe({ userInfo ->
+ val av =
+ findViewById(R.id.toolbar_avatar)
+ av.setImageBitmap(userInfo.avatar)
+ av.setOnClickListener { onOpen() }
+ if (video?.description.isNullOrEmpty() && actionBar != null) {
+ actionBar.subtitle = userInfo.owner.fullName
+ }
+ }) { })
+ } else {
+ findViewById(R.id.toolbar_avatar).visibility = View.GONE
+ }
+ mControllerView?.updateComment(!isLocal && video?.isCanComment == true)
+ }
+ mControllerView = VideoControllerView(this)
+ mSurfaceView = findViewById(R.id.videoSurface)
+ if (Settings.get().other().isVideo_swipes) {
+ attach(
+ this,
+ SlidrConfig.Builder().setAlphaForView(false).fromUnColoredToColoredStatusBar(true)
+ .position(SlidrPosition.LEFT)
+ .listener(object : SlidrListener {
+ override fun onSlideStateChanged(state: Int) {
+ if (state != STATE_IDLE) {
+ if (supportActionBar?.isShowing == true) {
+ resolveControlsVisibility()
+ }
+ }
+ }
+
+ @SuppressLint("Range")
+ override fun onSlideChange(percent: Float) {
+ var tmp = 1f - percent
+ tmp *= 4
+ tmp = Utils.clamp(1f - tmp, 0f, 1f)
+ if (Utils.hasOreo()) {
+ surfaceContainer?.setBackgroundColor(Color.argb(tmp, 0f, 0f, 0f))
+ } else {
+ surfaceContainer?.setBackgroundColor(
+ Color.argb(
+ (tmp * 255).toInt(),
+ 0,
+ 0,
+ 0
+ )
+ )
+ }
+ mControllerView?.alpha = tmp
+ toolbar?.alpha = tmp
+ mSurfaceView?.alpha = Utils.clamp(percent, 0f, 1f)
+ }
+
+ override fun onSlideOpened() {
+
+ }
+
+ override fun onSlideClosed(): Boolean {
+ finish()
+ overridePendingTransition(0, 0)
+ return true
+ }
+
+ }).build()
+ )
+ }
+ surfaceContainer.setOnClickListener { resolveControlsVisibility() }
+ val videoHolder = mSurfaceView?.holder
+ videoHolder?.addCallback(this)
+ resolveControlsVisibility()
+ mPlayer = createPlayer()
+ mPlayer?.addVideoSizeChangeListener(this)
+ mPlayer?.play()
+ if (seekSave > 0) {
+ mPlayer?.seekTo(seekSave)
+ }
+ mSpeed = findViewById(R.id.toolbar_play_speed)
+ Utils.setTint(
+ mSpeed,
+ if (mPlayer?.isPlaybackSpeed == true) CurrentTheme.getColorPrimary(this) else Color.parseColor(
+ "#ffffff"
+ )
+ )
+ mSpeed?.setOnClickListener {
+ mPlayer?.togglePlaybackSpeed()
+ Utils.setTint(
+ mSpeed,
+ if (mPlayer?.isPlaybackSpeed == true) CurrentTheme.getColorPrimary(this) else Color.parseColor(
+ "#ffffff"
+ )
+ )
+ }
+ mControllerView?.setMediaPlayer(this)
+ if (Settings.get().other().isVideo_controller_to_decor) {
+ mControllerView?.setAnchorView(mDecorView as ViewGroup?, true)
+ } else {
+ mControllerView?.setAnchorView(findViewById(R.id.panel), false)
+ }
+ mControllerView?.updateComment(!isLocal && video != null && video?.isCanComment == true)
+ mControllerView?.updatePip(
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && packageManager.hasSystemFeature(
+ PackageManager.FEATURE_PICTURE_IN_PICTURE
+ ) && hasPipPermission()
+ )
+ }
+
+ private fun createPlayer(): IVideoPlayer {
+ val settings = proxySettings
+ val config = settings.activeProxy
+ val url = fileUrl
+ return ExoVideoPlayer(
+ this,
+ url,
+ config,
+ size, object : IVideoPlayer.IUpdatePlayListener {
+ override fun onPlayChanged(isPause: Boolean) {
+ mControllerView?.updatePausePlay()
+ }
+ }
+ )
+ }
+
+ @Suppress("DEPRECATION")
+ private fun resolveControlsVisibility() {
+ val actionBar = supportActionBar ?: return
+ if (actionBar.isShowing) {
+ actionBar.hide()
+ mControllerView?.hide()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) mDecorView?.layoutParams =
+ WindowManager.LayoutParams(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)
+ mDecorView?.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_IMMERSIVE)
+ } else {
+ actionBar.show()
+ mControllerView?.show()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) mDecorView?.layoutParams =
+ WindowManager.LayoutParams(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT)
+ mDecorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ }
+ }
+
+ override fun onDestroy() {
+ mPlayer?.release()
+ super.onDestroy()
+ }
+
+ @Suppress("DEPRECATION")
+ override fun onResume() {
+ super.onResume()
+ ActivityFeatures.Builder()
+ .begin()
+ .setHideNavigationMenu(true)
+ .setBarsColored(colored = false, invertIcons = false)
+ .build()
+ .apply(this)
+ onStopCalled = false
+ val actionBar = supportActionBar
+ if (actionBar != null && actionBar.isShowing) {
+ actionBar.hide()
+ mControllerView?.hide()
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) mDecorView?.layoutParams =
+ WindowManager.LayoutParams(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)
+ mDecorView?.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_IMMERSIVE)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun onPictureInPictureModeChanged(
+ isInPictureInPictureMode: Boolean,
+ newConfig: Configuration
+ ) {
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
+ val actionBar = supportActionBar ?: return
+ if (isInPictureInPictureMode) {
+ actionBar.hide()
+ mControllerView?.hide()
+ } else {
+ if (onStopCalled) {
+ finish()
+ } else {
+ actionBar.show()
+ mControllerView?.show()
+ }
+ }
+ }
+
+ private fun canVideoPause(): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ !isInPictureInPictureMode
+ } else true
+ }
+
+ override fun onPause() {
+ if (canVideoPause()) {
+ if (!doNotPause) {
+ mPlayer?.pause()
+ }
+ mControllerView?.updatePausePlay()
+ }
+ super.onPause()
+ }
+
+ override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
+ override fun surfaceCreated(holder: SurfaceHolder) {
+ mPlayer?.setSurfaceHolder(holder)
+ }
+
+ override fun surfaceDestroyed(holder: SurfaceHolder) {}
+ override fun canPause(): Boolean {
+ return true
+ }
+
+ override fun canSeekBackward(): Boolean {
+ return true
+ }
+
+ override fun canSeekForward(): Boolean {
+ return true
+ }
+
+ override val bufferPercentage: Int
+ get() = mPlayer?.bufferPercentage ?: 0
+ override val currentPosition: Long
+ get() = mPlayer?.currentPosition ?: 0
+ override val bufferPosition: Long
+ get() = mPlayer?.bufferPosition ?: 0
+ override val duration: Long
+ get() = mPlayer?.duration ?: 0
+ override val isPlaying: Boolean
+ get() = mPlayer?.isPlaying == true
+
+ override fun pause() {
+ mPlayer?.pause()
+ }
+
+ override fun seekTo(pos: Long) {
+ mPlayer?.seekTo(pos)
+ }
+
+ override fun start() {
+ mPlayer?.play()
+ }
+
+ override val isFullScreen: Boolean
+ get() = false
+
+ override fun commentClick() {
+ val intent = Intent(this, SwipebleActivity::class.java)
+ intent.action = MainActivity.ACTION_OPEN_PLACE
+ val commented = Commented.from(video ?: return)
+ intent.putExtra(
+ Extra.PLACE,
+ PlaceFactory.getCommentsPlace(Settings.get().accounts().current, commented, null)
+ )
+ doNotPause = true
+ applyIntent(intent)
+ requestSwipeble.launch(intent)
+ }
+
+ override fun toggleFullScreen() {
+ try {
+ requestedOrientation =
+ if (isLandscape) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ } catch (e: Exception) {
+ CustomToast.createCustomToast(this).showToastError(R.string.not_supported)
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ private fun hasPipPermission(): Boolean {
+ val appsOps = getSystemService(APP_OPS_SERVICE) as AppOpsManager?
+ return when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
+ appsOps?.unsafeCheckOpNoThrow(
+ AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
+ Process.myUid(),
+ packageName
+ ) == AppOpsManager.MODE_ALLOWED
+ }
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
+ appsOps?.checkOpNoThrow(
+ AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
+ Process.myUid(),
+ packageName
+ ) == AppOpsManager.MODE_ALLOWED
+ }
+ else -> {
+ false
+ }
+ }
+ }
+
+ override fun toPIPScreen() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
+ && hasPipPermission()
+ ) if (!isInPictureInPictureMode) {
+ val aspectRatio = Rational(mSurfaceView?.width ?: 0, mSurfaceView?.height ?: 0)
+ enterPictureInPictureMode(
+ PictureInPictureParams.Builder().setAspectRatio(aspectRatio).build()
+ )
+ }
+ }
+ }
+
+ private val fileUrl: String
+ get() = when (size) {
+ InternalVideoSize.SIZE_240 -> video?.mp4link240 ?: run { finish(); return "null" }
+ InternalVideoSize.SIZE_360 -> video?.mp4link360 ?: run { finish(); return "null" }
+ InternalVideoSize.SIZE_480 -> video?.mp4link480 ?: run { finish(); return "null" }
+ InternalVideoSize.SIZE_720 -> video?.mp4link720 ?: run { finish(); return "null" }
+ InternalVideoSize.SIZE_1080 -> video?.mp4link1080 ?: run { finish(); return "null" }
+ InternalVideoSize.SIZE_1440 -> video?.mp4link1440 ?: run { finish(); return "null" }
+ InternalVideoSize.SIZE_2160 -> video?.mp4link2160 ?: run { finish(); return "null" }
+ InternalVideoSize.SIZE_HLS -> video?.hls ?: run { finish(); return "null" }
+ InternalVideoSize.SIZE_LIVE -> video?.live ?: run { finish(); return "null" }
+ else -> {
+ finish()
+ "null"
+ }
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ isLandscape = true
+ } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
+ isLandscape = false
+ }
+ }
+
+ override fun onVideoSizeChanged(player: IVideoPlayer, size: VideoSize) {
+ mSurfaceView?.setAspectRatio(size.width, size.height)
+ }
+
+ override fun hideMenu(hide: Boolean) {}
+ override fun openMenu(open: Boolean) {}
+
+ @Suppress("DEPRECATION")
+ override fun setStatusbarColored(colored: Boolean, invertIcons: Boolean) {
+ val statusbarNonColored = CurrentTheme.getStatusBarNonColored(this)
+ val statusbarColored = CurrentTheme.getStatusBarColor(this)
+ val w = window
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.statusBarColor = if (colored) statusbarColored else statusbarNonColored
+ @ColorInt val navigationColor =
+ if (colored) CurrentTheme.getNavigationBarColor(this) else Color.BLACK
+ w.navigationBarColor = navigationColor
+ if (Utils.hasMarshmallow()) {
+ var flags = window.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ }
+ window.decorView.systemUiVisibility = flags
+ }
+ if (Utils.hasOreo()) {
+ var flags = window.decorView.systemUiVisibility
+ if (invertIcons) {
+ flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ w.decorView.systemUiVisibility = flags
+ w.navigationBarColor = Color.WHITE
+ } else {
+ flags = flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ w.decorView.systemUiVisibility = flags
+ }
+ }
+ }
+
+ companion object {
+ const val EXTRA_VIDEO = "video"
+ const val EXTRA_SIZE = "size"
+ const val EXTRA_LOCAL = "local"
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/VideoSelectActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/VideoSelectActivity.kt
new file mode 100644
index 000000000..0c7773db5
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/VideoSelectActivity.kt
@@ -0,0 +1,87 @@
+package dev.ragnarok.fenrir.activity
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.fragment.search.SingleTabSearchFragment
+import dev.ragnarok.fenrir.fragment.videos.IVideosListView
+import dev.ragnarok.fenrir.fragment.videos.VideosFragment
+import dev.ragnarok.fenrir.fragment.videos.VideosTabsFragment
+import dev.ragnarok.fenrir.getParcelableCompat
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceProvider
+import dev.ragnarok.fenrir.util.Utils
+
+class VideoSelectActivity : NoMainActivity(), PlaceProvider {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ val accountId = (intent.extras ?: return).getInt(Extra.ACCOUNT_ID)
+ val ownerId = (intent.extras ?: return).getInt(Extra.OWNER_ID)
+ attachInitialFragment(accountId, ownerId)
+ }
+ }
+
+ private fun attachInitialFragment(accountId: Int, ownerId: Int) {
+ val fragment =
+ VideosTabsFragment.newInstance(accountId, ownerId, IVideosListView.ACTION_SELECT)
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(getMainContainerViewId(), fragment)
+ .addToBackStack("video-tabs")
+ .commit()
+ }
+
+ override fun openPlace(place: Place) {
+ when (place.type) {
+ Place.VIDEO_ALBUM -> {
+ val fragment: Fragment = VideosFragment.newInstance(place.safeArguments())
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(getMainContainerViewId(), fragment)
+ .addToBackStack("video-album")
+ .commit()
+ }
+ Place.SINGLE_SEARCH -> {
+ val singleTabSearchFragment =
+ SingleTabSearchFragment.newInstance(place.safeArguments())
+ supportFragmentManager
+ .beginTransaction()
+ .setCustomAnimations(R.anim.fragment_enter_pop, R.anim.fragment_exit_pop)
+ .replace(getMainContainerViewId(), singleTabSearchFragment)
+ .addToBackStack("video-search")
+ .commit()
+ }
+ Place.VIDEO_PREVIEW -> {
+ setResult(
+ RESULT_OK, Intent().putParcelableArrayListExtra(
+ Extra.ATTACHMENTS, Utils.singletonArrayList(
+ place.safeArguments().getParcelableCompat(
+ Extra.VIDEO
+ )
+ )
+ )
+ )
+ finish()
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * @param accountId От чьего имени получать
+ * @param ownerId Чьи получать
+ */
+
+ fun createIntent(context: Context, accountId: Int, ownerId: Int): Intent {
+ return Intent(context, VideoSelectActivity::class.java)
+ .putExtra(Extra.ACCOUNT_ID, accountId)
+ .putExtra(Extra.OWNER_ID, ownerId)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/BlackFenrirAlias.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/BlackFenrirAlias.kt
new file mode 100644
index 000000000..f6b078b8d
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/BlackFenrirAlias.kt
@@ -0,0 +1,6 @@
+package dev.ragnarok.fenrir.activity.alias
+
+import androidx.annotation.Keep
+
+@Keep
+class BlackFenrirAlias
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/BlueFenrirAlias.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/BlueFenrirAlias.kt
new file mode 100644
index 000000000..1a23e8c2a
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/BlueFenrirAlias.kt
@@ -0,0 +1,6 @@
+package dev.ragnarok.fenrir.activity.alias
+
+import androidx.annotation.Keep
+
+@Keep
+class BlueFenrirAlias
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/DefaultFenrirAlias.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/DefaultFenrirAlias.kt
new file mode 100644
index 000000000..bd6ced652
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/DefaultFenrirAlias.kt
@@ -0,0 +1,6 @@
+package dev.ragnarok.fenrir.activity.alias
+
+import androidx.annotation.Keep
+
+@Keep
+class DefaultFenrirAlias
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/GreenFenrirAlias.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/GreenFenrirAlias.kt
new file mode 100644
index 000000000..827b9ea58
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/GreenFenrirAlias.kt
@@ -0,0 +1,6 @@
+package dev.ragnarok.fenrir.activity.alias
+
+import androidx.annotation.Keep
+
+@Keep
+class GreenFenrirAlias
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/LineageFenrirAlias.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/LineageFenrirAlias.kt
new file mode 100644
index 000000000..43cead0e5
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/LineageFenrirAlias.kt
@@ -0,0 +1,6 @@
+package dev.ragnarok.fenrir.activity.alias
+
+import androidx.annotation.Keep
+
+@Keep
+class LineageFenrirAlias
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/RedFenrirAlias.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/RedFenrirAlias.kt
new file mode 100644
index 000000000..ec0f36c1e
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/RedFenrirAlias.kt
@@ -0,0 +1,6 @@
+package dev.ragnarok.fenrir.activity.alias
+
+import androidx.annotation.Keep
+
+@Keep
+class RedFenrirAlias
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/ToggleAlias.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/ToggleAlias.kt
new file mode 100644
index 000000000..40d99bc7f
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/ToggleAlias.kt
@@ -0,0 +1,99 @@
+package dev.ragnarok.fenrir.activity.alias
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import dev.ragnarok.fenrir.Constants
+
+object ToggleAlias {
+ private val aliases: Array> =
+ arrayOf(
+ BlueFenrirAlias::class.java,
+ GreenFenrirAlias::class.java,
+ VioletFenrirAlias::class.java,
+ RedFenrirAlias::class.java,
+ YellowFenrirAlias::class.java,
+ BlackFenrirAlias::class.java,
+ VKFenrirAlias::class.java,
+ WhiteFenrirAlias::class.java,
+ LineageFenrirAlias::class.java
+ )
+
+ fun reset(context: Context) {
+ for (i in aliases) {
+ if (context.packageManager.getComponentEnabledSetting(
+ ComponentName(
+ context,
+ i
+ )
+ ) != PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+ ) {
+ context.packageManager.setComponentEnabledSetting(
+ ComponentName(context, i),
+ PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
+ PackageManager.DONT_KILL_APP
+ )
+ }
+ }
+ if (context.packageManager.getComponentEnabledSetting(
+ ComponentName(
+ context,
+ DefaultFenrirAlias::class.java
+ )
+ ) != PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+ ) {
+ context.packageManager.setComponentEnabledSetting(
+ ComponentName(context, DefaultFenrirAlias::class.java),
+ PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
+ PackageManager.DONT_KILL_APP
+ )
+ }
+ }
+
+ fun toggleTo(context: Context, v: Class) {
+ if (context.packageManager.getComponentEnabledSetting(
+ ComponentName(
+ context,
+ DefaultFenrirAlias::class.java
+ )
+ ) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED && !Constants.IS_DEBUG
+ ) {
+ context.packageManager.setComponentEnabledSetting(
+ ComponentName(context, DefaultFenrirAlias::class.java),
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP
+ )
+ }
+ for (i in aliases) {
+ if (i == v) {
+ continue
+ }
+ if (context.packageManager.getComponentEnabledSetting(
+ ComponentName(
+ context,
+ i
+ )
+ ) != PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+ ) {
+ context.packageManager.setComponentEnabledSetting(
+ ComponentName(context, i),
+ PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
+ PackageManager.DONT_KILL_APP
+ )
+ }
+ }
+ if (context.packageManager.getComponentEnabledSetting(
+ ComponentName(
+ context,
+ v
+ )
+ ) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ ) {
+ context.packageManager.setComponentEnabledSetting(
+ ComponentName(context, v),
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/VKFenrirAlias.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/VKFenrirAlias.kt
new file mode 100644
index 000000000..315be3f67
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/VKFenrirAlias.kt
@@ -0,0 +1,6 @@
+package dev.ragnarok.fenrir.activity.alias
+
+import androidx.annotation.Keep
+
+@Keep
+class VKFenrirAlias
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/VioletFenrirAlias.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/VioletFenrirAlias.kt
new file mode 100644
index 000000000..d5688f7bb
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/VioletFenrirAlias.kt
@@ -0,0 +1,6 @@
+package dev.ragnarok.fenrir.activity.alias
+
+import androidx.annotation.Keep
+
+@Keep
+class VioletFenrirAlias
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/WhiteFenrirAlias.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/WhiteFenrirAlias.kt
new file mode 100644
index 000000000..724356a5c
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/WhiteFenrirAlias.kt
@@ -0,0 +1,6 @@
+package dev.ragnarok.fenrir.activity.alias
+
+import androidx.annotation.Keep
+
+@Keep
+class WhiteFenrirAlias
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/YellowFenrirAlias.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/YellowFenrirAlias.kt
new file mode 100644
index 000000000..57e27427b
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/alias/YellowFenrirAlias.kt
@@ -0,0 +1,6 @@
+package dev.ragnarok.fenrir.activity.alias
+
+import androidx.annotation.Keep
+
+@Keep
+class YellowFenrirAlias
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/crash/CrashUtils.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/crash/CrashUtils.kt
new file mode 100644
index 000000000..d1b9883cd
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/crash/CrashUtils.kt
@@ -0,0 +1,166 @@
+package dev.ragnarok.fenrir.activity.crash
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Process
+import android.util.Log
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.activity.MainActivity
+import dev.ragnarok.fenrir.util.PersistentLogger
+import dev.ragnarok.fenrir.util.Utils
+import java.io.PrintWriter
+import java.io.StringWriter
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.*
+
+object CrashUtils {
+ private const val TAG = "CrashUtils"
+
+ fun install(context: Context) {
+ try {
+ Thread.setDefaultUncaughtExceptionHandler { _: Thread?, throwable: Throwable ->
+ if (isStackTraceLikelyConflictive(
+ throwable,
+ DefaultErrorActivity::class.java
+ )
+ ) {
+ throwable.printStackTrace()
+ killCurrentProcess()
+ } else {
+ PersistentLogger.logThrowableSync("APP CRASH", throwable)
+ val intent = Intent(context, DefaultErrorActivity::class.java)
+ val sw = StringWriter()
+ val pw = PrintWriter(sw)
+ throwable.printStackTrace(pw)
+ var stackTraceString = sw.toString()
+ if (stackTraceString.length > 1677721) {
+ val disclaimer = " [stack trace too large]"
+ stackTraceString = stackTraceString.substring(
+ 0,
+ 1677721 - disclaimer.length
+ ) + disclaimer
+ }
+ intent.putExtra(Extra.STACK_TRACE, stackTraceString)
+ intent.putExtra(Extra.IS_OUT_OF_MEMORY, throwable is OutOfMemoryError)
+
+ intent.flags =
+ Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ context.startActivity(intent)
+ killCurrentProcess()
+ }
+ }
+ Log.i(TAG, "CrashActivity has been installed.")
+ } catch (t: Throwable) {
+ Log.e(
+ TAG,
+ "An unknown error occurred while installing CrashActivity, it may not have been properly initialized. Please report this as a bug if needed.",
+ t
+ )
+ }
+ }
+
+ fun closeApplication(activity: Activity) {
+ activity.finish()
+ killCurrentProcess()
+ }
+
+ private fun getStackTraceFromIntent(intent: Intent): String? {
+ return intent.getStringExtra(Extra.STACK_TRACE)
+ }
+
+ private val androidVersion: String
+ get() {
+ val release = Build.VERSION.RELEASE
+ val sdkVersion = Build.VERSION.SDK_INT
+ return "Android SDK: $sdkVersion ($release)"
+ }
+
+ fun getAllErrorDetailsFromIntent(context: Context, intent: Intent): String {
+ val currentDate = Date()
+ val dateFormat: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
+ val versionName = getVersionName(context)
+ var errorDetails = ""
+ errorDetails += "Build version: $versionName \n"
+ errorDetails += "Current date: ${dateFormat.format(currentDate)}\n"
+ errorDetails += "Android: $androidVersion\n"
+ errorDetails += "Device: $deviceModelName\n"
+ errorDetails += "Stack trace:\n"
+ errorDetails += getStackTraceFromIntent(intent)
+ return errorDetails
+ }
+
+ private fun restartApplicationWithIntent(
+ activity: Activity,
+ intent: Intent
+ ) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
+ activity.finish()
+ activity.startActivity(intent)
+ killCurrentProcess()
+ }
+
+ fun restartApplication(activity: Activity) {
+ val intent = Intent(activity, MainActivity::class.java)
+ restartApplicationWithIntent(activity, intent)
+ }
+
+ private fun isStackTraceLikelyConflictive(
+ throwable: Throwable,
+ activityClass: Class
+ ): Boolean {
+ var pThrowable: Throwable? = throwable
+ do {
+ val stackTrace = throwable.stackTrace
+ for (element in stackTrace) {
+ if (element.className == "android.app.ActivityThread" && element.methodName == "handleBindApplication" || element.className == activityClass.name) {
+ return true
+ }
+ }
+ } while (pThrowable?.cause.also { pThrowable = it } != null)
+ return false
+ }
+
+ @Suppress("deprecation")
+ private fun getVersionName(context: Context): String {
+ return try {
+ val packageInfo = if (Utils.hasTiramisu()) context.packageManager.getPackageInfo(
+ context.packageName,
+ PackageManager.PackageInfoFlags.of(0)
+ ) else context.packageManager.getPackageInfo(context.packageName, 0)
+ packageInfo.versionName
+ } catch (e: Exception) {
+ "Unknown"
+ }
+ }
+
+ private val deviceModelName: String
+ get() {
+ val manufacturer = Build.MANUFACTURER
+ val model = Build.MODEL
+ return if (model.startsWith(manufacturer)) {
+ capitalize(model)
+ } else {
+ capitalize(manufacturer) + " " + model
+ }
+ }
+
+ private fun capitalize(s: String?): String {
+ if (s.isNullOrEmpty()) {
+ return ""
+ }
+ val first = s[0]
+ return if (Character.isUpperCase(first)) {
+ s
+ } else {
+ Character.toUpperCase(first).toString() + s.substring(1)
+ }
+ }
+
+ private fun killCurrentProcess() {
+ Process.killProcess(Process.myPid())
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/crash/DefaultErrorActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/crash/DefaultErrorActivity.kt
new file mode 100644
index 000000000..2507817f4
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/crash/DefaultErrorActivity.kt
@@ -0,0 +1,91 @@
+package dev.ragnarok.fenrir.activity.crash
+
+import android.annotation.SuppressLint
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.DialogInterface
+import android.os.Bundle
+import android.util.TypedValue
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.slidr.Slidr
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrListener
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.util.toast.CustomToast
+
+class DefaultErrorActivity : AppCompatActivity() {
+ @SuppressLint("PrivateResource")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ setTheme(R.style.App_CrashError)
+ super.onCreate(savedInstanceState)
+ Slidr.attach(
+ this,
+ SlidrConfig.Builder().scrimColor(CurrentTheme.getColorBackground(this))
+ .listener(object : SlidrListener {
+ override fun onSlideStateChanged(state: Int) {
+ }
+
+ override fun onSlideChange(percent: Float) {
+ }
+
+ override fun onSlideOpened() {
+ }
+
+ override fun onSlideClosed(): Boolean {
+ CrashUtils.closeApplication(this@DefaultErrorActivity)
+ return true
+ }
+ }).build()
+ )
+ setContentView(R.layout.crash_error_activity)
+ findViewById(R.id.crash_error_activity_restart_button).setOnClickListener {
+ CrashUtils.restartApplication(
+ this
+ )
+ }
+
+ if (intent.getBooleanExtra(Extra.IS_OUT_OF_MEMORY, false)) {
+ findViewById(R.id.crash_error_activity_more_info_button).visibility =
+ View.GONE
+ findViewById(R.id.crash_error_activity_bag).visibility = View.GONE
+ findViewById(R.id.crash_error_activity_throwable).setText(R.string.crash_error_activity_out_of_memory)
+ }
+
+ findViewById(R.id.crash_error_activity_more_info_button).setOnClickListener {
+ val dialog = MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.crash_error_activity_error_details_title)
+ .setMessage(CrashUtils.getAllErrorDetailsFromIntent(this, intent))
+ .setPositiveButton(R.string.crash_error_activity_error_details_close, null)
+ .setNeutralButton(
+ R.string.crash_error_activity_error_details_copy
+ ) { _: DialogInterface?, _: Int -> copyErrorToClipboard() }
+ .show()
+ val textView = dialog.findViewById(android.R.id.message)
+ textView?.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX,
+ resources.getDimension(R.dimen.crash_error_activity_error_details_text_size)
+ )
+ }
+ }
+
+ private fun copyErrorToClipboard() {
+ val errorInformation = CrashUtils.getAllErrorDetailsFromIntent(this, intent)
+ val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager?
+ if (clipboard != null) {
+ val clip = ClipData.newPlainText(
+ getString(R.string.crash_error_activity_error_details_clipboard_label),
+ errorInformation
+ )
+ clipboard.setPrimaryClip(clip)
+ CustomToast.createCustomToast(this)
+ .showToastInfo(R.string.crash_error_activity_error_details_copied)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/gifpager/GifPagerActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/gifpager/GifPagerActivity.kt
new file mode 100644
index 000000000..88293b905
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/gifpager/GifPagerActivity.kt
@@ -0,0 +1,325 @@
+package dev.ragnarok.fenrir.activity.gifpager
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.view.*
+import android.widget.RelativeLayout
+import androidx.annotation.ColorInt
+import androidx.annotation.LayoutRes
+import androidx.annotation.StringRes
+import androidx.appcompat.widget.Toolbar
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.ActivityFeatures
+import dev.ragnarok.fenrir.activity.slidr.Slidr.attach
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrListener
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrPosition
+import dev.ragnarok.fenrir.fragment.absdocumentpreview.AbsDocumentPreviewActivity
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment
+import dev.ragnarok.fenrir.fragment.base.core.IPresenterFactory
+import dev.ragnarok.fenrir.getParcelableArrayListCompat
+import dev.ragnarok.fenrir.listener.AppStyleable
+import dev.ragnarok.fenrir.model.Document
+import dev.ragnarok.fenrir.model.PhotoSize
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceProvider
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.util.AppPerms.requestPermissionsAbs
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.view.CircleCounterButton
+import dev.ragnarok.fenrir.view.TouchImageView
+
+class GifPagerActivity : AbsDocumentPreviewActivity(),
+ IGifPagerView, PlaceProvider, AppStyleable {
+ private var mViewPager: ViewPager2? = null
+ private var mToolbar: Toolbar? = null
+ private var mButtonsRoot: View? = null
+ private var mButtonAddOrDelete: CircleCounterButton? = null
+ private var mFullscreen = false
+
+ @LayoutRes
+ override fun getNoMainContentView(): Int {
+ return R.layout.fragment_gif_pager
+ }
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ mFullscreen = savedInstanceState?.getBoolean("mFullscreen") ?: false
+ mToolbar = findViewById(R.id.toolbar)
+ val mContentRoot = findViewById(R.id.gif_pager_root)
+ setSupportActionBar(mToolbar)
+ mButtonsRoot = findViewById(R.id.buttons)
+ mButtonAddOrDelete = findViewById(R.id.button_add_or_delete)
+ mButtonAddOrDelete?.setOnClickListener {
+ presenter?.fireAddDeleteButtonClick()
+ }
+ mViewPager = findViewById(R.id.view_pager)
+ mViewPager?.offscreenPageLimit = 1
+ mViewPager?.setPageTransformer(
+ Utils.createPageTransform(
+ Settings.get().main().viewpager_page_transform
+ )
+ )
+ mViewPager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+ override fun onPageSelected(position: Int) {
+ super.onPageSelected(position)
+ presenter?.selectPage(
+ position
+ )
+ }
+ })
+ attach(
+ this,
+ SlidrConfig.Builder().setAlphaForView(false).fromUnColoredToColoredStatusBar(true)
+ .position(SlidrPosition.VERTICAL)
+ .listener(object : SlidrListener {
+ override fun onSlideStateChanged(state: Int) {
+
+ }
+
+ @SuppressLint("Range")
+ override fun onSlideChange(percent: Float) {
+ var tmp = 1f - percent
+ tmp *= 4
+ tmp = Utils.clamp(1f - tmp, 0f, 1f)
+ if (Utils.hasOreo()) {
+ mContentRoot?.setBackgroundColor(Color.argb(tmp, 0f, 0f, 0f))
+ } else {
+ mContentRoot?.setBackgroundColor(
+ Color.argb(
+ (tmp * 255).toInt(),
+ 0,
+ 0,
+ 0
+ )
+ )
+ }
+ mButtonsRoot?.alpha = tmp
+ mToolbar?.alpha = tmp
+ mViewPager?.alpha = Utils.clamp(percent, 0f, 1f)
+ }
+
+ override fun onSlideOpened() {
+
+ }
+
+ override fun onSlideClosed(): Boolean {
+ finish()
+ overridePendingTransition(0, 0)
+ return true
+ }
+
+ }).build()
+ )
+
+ findViewById(R.id.button_share).setOnClickListener {
+ presenter?.fireShareButtonClick()
+ }
+ findViewById(R.id.button_download).setOnClickListener {
+ presenter?.fireDownloadButtonClick(
+ this,
+ mContentRoot
+ )
+ }
+ resolveFullscreenViews()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putBoolean("mFullscreen", mFullscreen)
+ }
+
+ override val requestWritePermission = requestPermissionsAbs(
+ arrayOf(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ )
+ ) {
+ lazyPresenter {
+ fireWritePermissionResolved(
+ this@GifPagerActivity,
+ this@GifPagerActivity.findViewById(R.id.gif_pager_root)
+ )
+ }
+ }
+
+ override fun openPlace(place: Place) {
+ val args = place.safeArguments()
+ when (place.type) {
+ Place.PLAYER -> {
+ val player = supportFragmentManager.findFragmentByTag("audio_player")
+ if (player is AudioPlayerFragment) player.dismiss()
+ AudioPlayerFragment.newInstance(args).show(supportFragmentManager, "audio_player")
+ }
+ else -> Utils.openPlaceWithSwipebleActivity(this, place)
+ }
+ }
+
+ override fun hideMenu(hide: Boolean) {}
+ override fun openMenu(open: Boolean) {}
+
+ @Suppress("DEPRECATION")
+ override fun setStatusbarColored(colored: Boolean, invertIcons: Boolean) {
+ val statusbarNonColored = CurrentTheme.getStatusBarNonColored(this)
+ val statusbarColored = CurrentTheme.getStatusBarColor(this)
+ val w = window
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.statusBarColor = if (colored) statusbarColored else statusbarNonColored
+ @ColorInt val navigationColor =
+ if (colored) CurrentTheme.getNavigationBarColor(this) else Color.BLACK
+ w.navigationBarColor = navigationColor
+ if (Utils.hasMarshmallow()) {
+ var flags = window.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ }
+ window.decorView.systemUiVisibility = flags
+ }
+ if (Utils.hasOreo()) {
+ var flags = window.decorView.systemUiVisibility
+ if (invertIcons) {
+ flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ w.decorView.systemUiVisibility = flags
+ w.navigationBarColor = Color.WHITE
+ } else {
+ flags = flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ w.decorView.systemUiVisibility = flags
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ ActivityFeatures.Builder()
+ .begin()
+ .setHideNavigationMenu(true)
+ .setBarsColored(colored = false, invertIcons = false)
+ .build()
+ .apply(this)
+ }
+
+ internal fun toggleFullscreen() {
+ mFullscreen = !mFullscreen
+ resolveFullscreenViews()
+ }
+
+ private fun resolveFullscreenViews() {
+ mToolbar?.visibility = if (mFullscreen) View.GONE else View.VISIBLE
+ mButtonsRoot?.visibility = if (mFullscreen) View.GONE else View.VISIBLE
+ }
+
+ override fun getPresenterFactory(saveInstanceState: Bundle?): IPresenterFactory {
+ return object : IPresenterFactory {
+ override fun create(): GifPagerPresenter {
+ val aid = requireArguments().getInt(Extra.ACCOUNT_ID)
+ val index = requireArguments().getInt(Extra.INDEX)
+ val documents: ArrayList =
+ requireArguments().getParcelableArrayListCompat(Extra.DOCS)!!
+ return GifPagerPresenter(aid, documents, index, saveInstanceState)
+ }
+ }
+ }
+
+ override fun displayData(mDocuments: List, selectedIndex: Int) {
+ if (mViewPager != null) {
+ val adapter = Adapter(mDocuments)
+ mViewPager?.adapter = adapter
+ mViewPager?.setCurrentItem(selectedIndex, false)
+ }
+ }
+
+ override fun setupAddRemoveButton(addEnable: Boolean) {
+ mButtonAddOrDelete?.setIcon(if (addEnable) R.drawable.plus else R.drawable.ic_outline_delete)
+ }
+
+ inner class Holder internal constructor(rootView: View) : RecyclerView.ViewHolder(rootView) {
+ val mGifView: TouchImageView = rootView.findViewById(R.id.gif_view)
+
+ init {
+ mGifView.setOnClickListener { toggleFullscreen() }
+ }
+ }
+
+ override fun toolbarTitle(@StringRes titleRes: Int, vararg params: Any?) {
+ supportActionBar?.title = getString(titleRes, *params)
+ }
+
+ override fun toolbarSubtitle(@StringRes titleRes: Int, vararg params: Any?) {
+ supportActionBar?.subtitle = getString(titleRes, *params)
+ }
+
+ private inner class Adapter(private var data: List) :
+ RecyclerView.Adapter() {
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onCreateViewHolder(container: ViewGroup, viewType: Int): Holder {
+ val ret = Holder(
+ LayoutInflater.from(container.context)
+ .inflate(R.layout.content_gif_page, container, false)
+ )
+ ret.mGifView.setOnTouchListener { view: View, event: MotionEvent ->
+ if (event.pointerCount >= 2 || view.canScrollHorizontally(1) && view.canScrollHorizontally(
+ -1
+ )
+ ) {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
+ container.requestDisallowInterceptTouchEvent(true)
+ return@setOnTouchListener false
+ }
+ MotionEvent.ACTION_UP -> {
+ container.requestDisallowInterceptTouchEvent(false)
+ return@setOnTouchListener true
+ }
+ }
+ }
+ true
+ }
+ return ret
+ }
+
+ override fun onBindViewHolder(holder: Holder, position: Int) {
+ holder.mGifView.fromNet(
+ data[position].ownerId.toString() + "_" + data[position].id.toString(),
+ data[position].videoPreview?.src,
+ data[position].getPreviewWithSize(PhotoSize.W, false), Utils.createOkHttp(5, true)
+ )
+ }
+
+ override fun getItemCount(): Int {
+ return data.size
+ }
+ }
+
+ companion object {
+ const val ACTION_OPEN =
+ "dev.ragnarok.fenrir.activity.gifpager.GifPagerActivity"
+
+ fun newInstance(context: Context, args: Bundle?): Intent {
+ val ph = Intent(context, GifPagerActivity::class.java)
+ val targetArgs = Bundle()
+ targetArgs.putAll(args)
+ ph.action = ACTION_OPEN
+ ph.putExtras(targetArgs)
+ return ph
+ }
+
+ fun buildArgs(aid: Int, documents: ArrayList, index: Int): Bundle {
+ val args = Bundle()
+ args.putInt(Extra.ACCOUNT_ID, aid)
+ args.putInt(Extra.INDEX, index)
+ args.putParcelableArrayList(Extra.DOCS, documents)
+ return args
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/gifpager/GifPagerPresenter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/gifpager/GifPagerPresenter.kt
new file mode 100644
index 000000000..2c822a92a
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/gifpager/GifPagerPresenter.kt
@@ -0,0 +1,101 @@
+package dev.ragnarok.fenrir.activity.gifpager
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import com.google.android.material.snackbar.BaseTransientBottomBar
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.fragment.absdocumentpreview.BaseDocumentPresenter
+import dev.ragnarok.fenrir.model.Document
+import dev.ragnarok.fenrir.util.AppPerms.hasReadWriteStoragePermission
+import dev.ragnarok.fenrir.util.DownloadWorkUtils.doDownloadDoc
+import dev.ragnarok.fenrir.util.toast.CustomSnackbars
+
+class GifPagerPresenter(
+ accountId: Int,
+ private val mDocuments: ArrayList,
+ private var mCurrentIndex: Int,
+ savedInstanceState: Bundle?
+) : BaseDocumentPresenter(accountId, savedInstanceState) {
+
+ fun selectPage(position: Int) {
+ if (mCurrentIndex == position) {
+ return
+ }
+ mCurrentIndex = position
+ resolveToolbarSubtitle()
+ }
+
+ private fun resolveToolbarTitle() {
+ view?.toolbarTitle(R.string.gif_player)
+ }
+
+ private fun resolveToolbarSubtitle() {
+ view?.toolbarSubtitle(
+ R.string.image_number,
+ mCurrentIndex + 1,
+ mDocuments.size
+ )
+ }
+
+ private fun resolveAddDeleteButton() {
+ view?.setupAddRemoveButton(!isMy)
+ }
+
+ private val isMy: Boolean
+ get() = mDocuments[mCurrentIndex].ownerId == accountId
+
+ override fun onGuiCreated(viewHost: IGifPagerView) {
+ super.onGuiCreated(viewHost)
+ viewHost.displayData(mDocuments, mCurrentIndex)
+ resolveAddDeleteButton()
+ resolveToolbarTitle()
+ resolveToolbarSubtitle()
+ }
+
+ fun fireAddDeleteButtonClick() {
+ val document = mDocuments[mCurrentIndex]
+ if (isMy) {
+ delete(document.id, document.ownerId)
+ } else {
+ addYourself(document)
+ }
+ }
+
+ fun fireShareButtonClick() {
+ view?.shareDocument(
+ accountId,
+ mDocuments[mCurrentIndex]
+ )
+ }
+
+ fun fireDownloadButtonClick(context: Context, view: View?) {
+ if (!hasReadWriteStoragePermission(context)) {
+ resumedView?.requestWriteExternalStoragePermission()
+ return
+ }
+ downloadImpl(context, view)
+ }
+
+ public override fun onWritePermissionResolved(context: Context, view: View?) {
+ if (hasReadWriteStoragePermission(context)) {
+ downloadImpl(context, view)
+ }
+ }
+
+ private fun downloadImpl(context: Context, view: View?) {
+ val document = mDocuments[mCurrentIndex]
+ if (doDownloadDoc(context, document, false) == 1) {
+ CustomSnackbars.createCustomSnackbars(view)
+ ?.setDurationSnack(BaseTransientBottomBar.LENGTH_LONG)
+ ?.themedSnack(R.string.audio_force_download)
+ ?.setAction(
+ R.string.button_yes
+ ) {
+ doDownloadDoc(
+ context, document, true
+ )
+ }?.show()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/gifpager/IGifPagerView.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/gifpager/IGifPagerView.kt
new file mode 100644
index 000000000..ab3bdf43c
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/gifpager/IGifPagerView.kt
@@ -0,0 +1,12 @@
+package dev.ragnarok.fenrir.activity.gifpager
+
+import androidx.annotation.StringRes
+import dev.ragnarok.fenrir.fragment.absdocumentpreview.IBasicDocumentView
+import dev.ragnarok.fenrir.model.Document
+
+interface IGifPagerView : IBasicDocumentView {
+ fun displayData(mDocuments: List, selectedIndex: Int)
+ fun setupAddRemoveButton(addEnable: Boolean)
+ fun toolbarTitle(@StringRes titleRes: Int, vararg params: Any?)
+ fun toolbarSubtitle(@StringRes titleRes: Int, vararg params: Any?)
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/FavePhotoPagerPresenter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/FavePhotoPagerPresenter.kt
new file mode 100644
index 000000000..dd4afa623
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/FavePhotoPagerPresenter.kt
@@ -0,0 +1,71 @@
+package dev.ragnarok.fenrir.activity.photopager
+
+import android.content.Context
+import android.os.Bundle
+import dev.ragnarok.fenrir.fromIOToMain
+import dev.ragnarok.fenrir.model.AccessIdPair
+import dev.ragnarok.fenrir.model.Photo
+import dev.ragnarok.fenrir.util.Utils
+
+class FavePhotoPagerPresenter(
+ photos: ArrayList,
+ index: Int,
+ accountId: Int,
+ context: Context,
+ savedInstanceState: Bundle?
+) : PhotoPagerPresenter(photos, accountId, false, context, savedInstanceState) {
+ private val mUpdated: BooleanArray = BooleanArray(photos.size)
+ private val refreshing: BooleanArray = BooleanArray(photos.size)
+ override fun close() {
+ view?.returnOnlyPos(currentIndex)
+ }
+
+ private fun refresh(index: Int) {
+ if (mUpdated[index] || refreshing[index]) {
+ return
+ }
+ refreshing[index] = true
+ val photo = mPhotos[index]
+ val forUpdate = listOf(AccessIdPair(photo.getObjectId(), photo.ownerId, photo.accessKey))
+ appendDisposable(photosInteractor.getPhotosByIds(accountId, forUpdate)
+ .fromIOToMain()
+ .subscribe({ photos ->
+ onPhotoUpdateReceived(
+ photos,
+ index
+ )
+ }) { t -> onRefreshFailed(index, t) })
+ }
+
+ private fun onRefreshFailed(index: Int, t: Throwable) {
+ refreshing[index] = false
+ view?.let {
+ showError(
+ it,
+ Utils.getCauseIfRuntime(t)
+ )
+ }
+ }
+
+ private fun onPhotoUpdateReceived(result: List, index: Int) {
+ refreshing[index] = false
+ if (result.size == 1) {
+ val p = result[0]
+ mPhotos[index] = p
+ mUpdated[index] = true
+ if (currentIndex == index) {
+ refreshInfoViews(true)
+ }
+ }
+ }
+
+ override fun afterPageChangedFromUi(oldPage: Int, newPage: Int) {
+ super.afterPageChangedFromUi(oldPage, newPage)
+ refresh(newPage)
+ }
+
+ init {
+ currentIndex = index
+ refresh(index)
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/IPhotoPagerView.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/IPhotoPagerView.kt
new file mode 100644
index 000000000..0e440a799
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/IPhotoPagerView.kt
@@ -0,0 +1,31 @@
+package dev.ragnarok.fenrir.activity.photopager
+
+import dev.ragnarok.fenrir.fragment.base.core.IErrorView
+import dev.ragnarok.fenrir.fragment.base.core.IMvpView
+import dev.ragnarok.fenrir.fragment.base.core.IToastView
+import dev.ragnarok.fenrir.model.Commented
+import dev.ragnarok.fenrir.model.Photo
+
+interface IPhotoPagerView : IMvpView, IErrorView, IToastView {
+ fun goToLikesList(accountId: Int, ownerId: Int, photoId: Int)
+ fun setupLikeButton(visible: Boolean, like: Boolean, likes: Int)
+ fun setupWithUserButton(users: Int)
+ fun setupShareButton(visible: Boolean, reposts: Int)
+ fun setupCommentsButton(visible: Boolean, count: Int)
+ fun displayPhotos(photos: List, initialIndex: Int)
+ fun setToolbarTitle(title: String?)
+ fun setToolbarSubtitle(subtitle: String?)
+ fun sharePhoto(accountId: Int, photo: Photo)
+ fun postToMyWall(photo: Photo, accountId: Int)
+ fun requestWriteToExternalStoragePermission()
+ fun setButtonRestoreVisible(visible: Boolean)
+ fun setupOptionMenu(canSaveYourself: Boolean, canDelete: Boolean)
+ fun goToComments(accountId: Int, commented: Commented)
+ fun displayPhotoListLoading(loading: Boolean)
+ fun setButtonsBarVisible(visible: Boolean)
+ fun setToolbarVisible(visible: Boolean)
+ fun rebindPhotoAt(position: Int)
+ fun closeOnly()
+ fun returnInfo(position: Int, parcelNativePtr: Long)
+ fun returnOnlyPos(position: Int)
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/PhotoAlbumPagerPresenter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/PhotoAlbumPagerPresenter.kt
new file mode 100644
index 000000000..53d201ef8
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/PhotoAlbumPagerPresenter.kt
@@ -0,0 +1,195 @@
+package dev.ragnarok.fenrir.activity.photopager
+
+import android.content.Context
+import android.os.Bundle
+import dev.ragnarok.fenrir.db.Stores
+import dev.ragnarok.fenrir.db.serialize.Serializers
+import dev.ragnarok.fenrir.domain.ILocalServerInteractor
+import dev.ragnarok.fenrir.domain.InteractorFactory
+import dev.ragnarok.fenrir.fromIOToMain
+import dev.ragnarok.fenrir.model.Photo
+import dev.ragnarok.fenrir.model.TmpSource
+import dev.ragnarok.fenrir.module.FenrirNative
+import dev.ragnarok.fenrir.module.parcel.ParcelFlags
+import dev.ragnarok.fenrir.module.parcel.ParcelNative
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.util.PersistentLogger
+import dev.ragnarok.fenrir.util.Utils
+
+class PhotoAlbumPagerPresenter : PhotoPagerPresenter {
+ private val localServerInteractor: ILocalServerInteractor
+ private val mOwnerId: Int
+ private val mAlbumId: Int
+ private val invertPhotoRev: Boolean
+ private var canLoad: Boolean
+
+ constructor(
+ index: Int,
+ accountId: Int,
+ ownerId: Int,
+ albumId: Int,
+ photos: ArrayList,
+ readOnly: Boolean,
+ invertPhotoRev: Boolean,
+ context: Context,
+ savedInstanceState: Bundle?
+ ) : super(ArrayList(0), accountId, readOnly, context, savedInstanceState) {
+ localServerInteractor = InteractorFactory.createLocalServerInteractor()
+ mOwnerId = ownerId
+ mAlbumId = albumId
+ canLoad = true
+ this.invertPhotoRev = invertPhotoRev
+ mPhotos.addAll(photos)
+ currentIndex = index
+ refreshPagerView()
+ resolveButtonsBarVisible()
+ resolveToolbarVisibility()
+ refreshInfoViews(true)
+ }
+
+ constructor(
+ index: Int,
+ accountId: Int,
+ ownerId: Int,
+ albumId: Int,
+ source: TmpSource,
+ readOnly: Boolean,
+ invertPhotoRev: Boolean,
+ context: Context,
+ savedInstanceState: Bundle?
+ ) : super(ArrayList(0), accountId, readOnly, context, savedInstanceState) {
+ localServerInteractor = InteractorFactory.createLocalServerInteractor()
+ mOwnerId = ownerId
+ mAlbumId = albumId
+ canLoad = true
+ this.invertPhotoRev = invertPhotoRev
+ currentIndex = index
+ loadDataFromDatabase(source)
+ }
+
+ private fun loadDataFromDatabase(source: TmpSource) {
+ changeLoadingNowState(true)
+ appendDisposable(Stores.instance
+ .tempStore()
+ .getTemporaryData(source.ownerId, source.sourceId, Serializers.PHOTOS_SERIALIZER)
+ .fromIOToMain()
+ .subscribe({ onInitialLoadingFinished(it) }) {
+ PersistentLogger.logThrowable("PhotoAlbumPagerPresenter", it)
+ })
+ }
+
+ private fun onInitialLoadingFinished(photos: List) {
+ changeLoadingNowState(false)
+ mPhotos.addAll(photos)
+ refreshPagerView()
+ resolveButtonsBarVisible()
+ resolveToolbarVisibility()
+ refreshInfoViews(true)
+ }
+
+ override fun need_update_info(): Boolean {
+ return true
+ }
+
+ private fun loadData() {
+ if (!canLoad) return
+ changeLoadingNowState(true)
+ if (mAlbumId != -9001 && mAlbumId != -9000 && mAlbumId != -311) {
+ appendDisposable(photosInteractor[accountId, mOwnerId, mAlbumId, COUNT_PER_LOAD, mPhotos.size, !invertPhotoRev]
+ .fromIOToMain()
+ .subscribe({ onActualPhotosReceived(it) }) { t ->
+ onActualDataGetError(
+ t
+ )
+ })
+ } else if (mAlbumId == -9000) {
+ appendDisposable(photosInteractor.getUsersPhoto(
+ accountId,
+ mOwnerId,
+ 1,
+ if (invertPhotoRev) 1 else 0,
+ mPhotos.size,
+ COUNT_PER_LOAD
+ )
+ .fromIOToMain()
+ .subscribe({ onActualPhotosReceived(it) }) { t ->
+ onActualDataGetError(
+ t
+ )
+ })
+ } else if (mAlbumId == -9001) {
+ appendDisposable(photosInteractor.getAll(
+ accountId,
+ mOwnerId,
+ 1,
+ 1,
+ mPhotos.size,
+ COUNT_PER_LOAD
+ )
+ .fromIOToMain()
+ .subscribe({ onActualPhotosReceived(it) }) {
+ onActualDataGetError(
+ it
+ )
+ })
+ } else {
+ appendDisposable(localServerInteractor.getPhotos(
+ mPhotos.size,
+ COUNT_PER_LOAD,
+ invertPhotoRev
+ )
+ .fromIOToMain()
+ .subscribe({ onActualPhotosReceived(it) }) {
+ onActualDataGetError(
+ it
+ )
+ })
+ }
+ }
+
+ private fun onActualDataGetError(t: Throwable) {
+ view?.let {
+ showError(
+ it,
+ Utils.getCauseIfRuntime(t)
+ )
+ }
+ }
+
+ override fun close() {
+ if (Settings.get().other().isNative_parcel_photo && FenrirNative.isNativeLoaded) {
+ val ptr = ParcelNative.createParcelableList(mPhotos, ParcelFlags.NULL_LIST)
+ view?.returnInfo(
+ currentIndex,
+ ptr
+ )
+ } else {
+ view?.closeOnly()
+ }
+ }
+
+ private fun onActualPhotosReceived(data: List) {
+ changeLoadingNowState(false)
+ if (data.isEmpty()) {
+ canLoad = false
+ return
+ }
+ mPhotos.addAll(data)
+ refreshPagerView()
+ resolveButtonsBarVisible()
+ resolveToolbarVisibility()
+ refreshInfoViews(true)
+ }
+
+ override fun afterPageChangedFromUi(oldPage: Int, newPage: Int) {
+ super.afterPageChangedFromUi(oldPage, newPage)
+ if (oldPage == newPage) return
+ if (newPage == count() - 1) {
+ loadData()
+ }
+ }
+
+ companion object {
+ private const val COUNT_PER_LOAD = 100
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/PhotoPagerActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/PhotoPagerActivity.kt
new file mode 100644
index 000000000..0a5874c42
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/PhotoPagerActivity.kt
@@ -0,0 +1,925 @@
+package dev.ragnarok.fenrir.activity.photopager
+
+import android.Manifest
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.util.SparseIntArray
+import android.view.*
+import android.widget.RelativeLayout
+import androidx.activity.OnBackPressedCallback
+import androidx.annotation.ColorInt
+import androidx.annotation.IdRes
+import androidx.annotation.LayoutRes
+import androidx.appcompat.widget.PopupMenu
+import androidx.appcompat.widget.Toolbar
+import androidx.core.view.MenuProvider
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.squareup.picasso3.Callback
+import com.squareup.picasso3.Rotatable
+import dev.ragnarok.fenrir.*
+import dev.ragnarok.fenrir.activity.*
+import dev.ragnarok.fenrir.activity.slidr.Slidr
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrListener
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrPosition
+import dev.ragnarok.fenrir.domain.ILikesInteractor
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment
+import dev.ragnarok.fenrir.fragment.base.core.IPresenterFactory
+import dev.ragnarok.fenrir.fragment.base.horizontal.ImageListAdapter
+import dev.ragnarok.fenrir.listener.AppStyleable
+import dev.ragnarok.fenrir.model.*
+import dev.ragnarok.fenrir.module.FenrirNative
+import dev.ragnarok.fenrir.module.parcel.ParcelFlags
+import dev.ragnarok.fenrir.module.parcel.ParcelNative
+import dev.ragnarok.fenrir.picasso.PicassoInstance
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceFactory
+import dev.ragnarok.fenrir.place.PlaceProvider
+import dev.ragnarok.fenrir.place.PlaceUtil
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.util.AppPerms.requestPermissionsAbs
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.rxutils.RxUtils
+import dev.ragnarok.fenrir.util.toast.CustomToast.Companion.createCustomToast
+import dev.ragnarok.fenrir.view.CircleCounterButton
+import dev.ragnarok.fenrir.view.TouchImageView
+import dev.ragnarok.fenrir.view.natives.rlottie.RLottieImageView
+import dev.ragnarok.fenrir.view.pager.WeakPicassoLoadCallback
+import io.reactivex.rxjava3.core.Completable
+import io.reactivex.rxjava3.disposables.Disposable
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+class PhotoPagerActivity : BaseMvpActivity(), IPhotoPagerView,
+ PlaceProvider, AppStyleable, MenuProvider {
+ companion object {
+ private const val EXTRA_PHOTOS = "photos"
+ private const val EXTRA_NEED_UPDATE = "need_update"
+ private val SIZES = SparseIntArray()
+ private const val DEFAULT_PHOTO_SIZE = PhotoSize.W
+ private const val ACTION_OPEN =
+ "dev.ragnarok.fenrir.activity.photopager.PhotoPagerActivity"
+
+ fun buildArgsForSimpleGallery(
+ aid: Int, index: Int, photos: ArrayList,
+ needUpdate: Boolean
+ ): Bundle {
+ val args = Bundle()
+ args.putInt(Extra.ACCOUNT_ID, aid)
+ args.putParcelableArrayList(EXTRA_PHOTOS, photos)
+ args.putInt(Extra.INDEX, index)
+ args.putBoolean(EXTRA_NEED_UPDATE, needUpdate)
+ return args
+ }
+
+ fun buildArgsForAlbum(
+ aid: Int,
+ albumId: Int,
+ ownerId: Int,
+ source: TmpSource,
+ position: Int,
+ readOnly: Boolean,
+ invert: Boolean
+ ): Bundle {
+ val args = Bundle()
+ args.putInt(Extra.ACCOUNT_ID, aid)
+ args.putInt(Extra.OWNER_ID, ownerId)
+ args.putInt(Extra.ALBUM_ID, albumId)
+ args.putInt(Extra.INDEX, position)
+ args.putBoolean(Extra.READONLY, readOnly)
+ args.putBoolean(Extra.INVERT, invert)
+ args.putParcelable(Extra.SOURCE, source)
+ return args
+ }
+
+ fun buildArgsForAlbum(
+ aid: Int,
+ albumId: Int,
+ ownerId: Int,
+ photos: ArrayList,
+ position: Int,
+ readOnly: Boolean,
+ invert: Boolean
+ ): Bundle {
+ val args = Bundle()
+ args.putInt(Extra.ACCOUNT_ID, aid)
+ args.putInt(Extra.OWNER_ID, ownerId)
+ args.putInt(Extra.ALBUM_ID, albumId)
+ args.putInt(Extra.INDEX, position)
+ args.putBoolean(Extra.READONLY, readOnly)
+ args.putBoolean(Extra.INVERT, invert)
+ if (FenrirNative.isNativeLoaded && Settings.get().other().isNative_parcel_photo) {
+ args.putLong(
+ EXTRA_PHOTOS,
+ ParcelNative.createParcelableList(photos, ParcelFlags.NULL_LIST)
+ )
+ } else {
+ args.putParcelableArrayList(EXTRA_PHOTOS, photos)
+ }
+ return args
+ }
+
+ fun buildArgsForAlbum(
+ aid: Int,
+ albumId: Int,
+ ownerId: Int,
+ parcelNativePointer: Long,
+ position: Int,
+ readOnly: Boolean,
+ invert: Boolean
+ ): Bundle {
+ val args = Bundle()
+ args.putInt(Extra.ACCOUNT_ID, aid)
+ args.putInt(Extra.OWNER_ID, ownerId)
+ args.putInt(Extra.ALBUM_ID, albumId)
+ args.putInt(Extra.INDEX, position)
+ args.putBoolean(Extra.READONLY, readOnly)
+ args.putBoolean(Extra.INVERT, invert)
+ args.putLong(EXTRA_PHOTOS, parcelNativePointer)
+ return args
+ }
+
+ fun buildArgsForFave(aid: Int, photos: ArrayList, index: Int): Bundle {
+ val args = Bundle()
+ args.putInt(Extra.ACCOUNT_ID, aid)
+ args.putParcelableArrayList(EXTRA_PHOTOS, photos)
+ args.putInt(Extra.INDEX, index)
+ return args
+ }
+
+ private var mLastBackPressedTime: Long = 0
+
+ fun newInstance(context: Context, placeType: Int, args: Bundle?): Intent? {
+ if (mLastBackPressedTime + 1000 > System.currentTimeMillis()) {
+ return null
+ }
+ mLastBackPressedTime = System.currentTimeMillis()
+ val ph = Intent(context, PhotoPagerActivity::class.java)
+ val targetArgs = Bundle()
+ targetArgs.putAll(args)
+ targetArgs.putInt(Extra.PLACE_TYPE, placeType)
+ ph.action = ACTION_OPEN
+ ph.putExtras(targetArgs)
+ return ph
+ }
+
+ internal fun addPhotoSizeToMenu(menu: PopupMenu, id: Int, size: Int, selectedItem: Int) {
+ menu.menu
+ .add(0, id, 0, getTitleForPhotoSize(size)).isChecked = selectedItem == size
+ }
+
+ internal fun getTitleForPhotoSize(size: Int): String {
+ return when (size) {
+ PhotoSize.X -> 604.toString() + "px"
+ PhotoSize.Y -> 807.toString() + "px"
+ PhotoSize.Z -> 1024.toString() + "px"
+ PhotoSize.W -> 2048.toString() + "px"
+ else -> throw IllegalArgumentException("Unsupported size")
+ }
+ }
+
+ init {
+ SIZES.put(1, PhotoSize.X)
+ SIZES.put(2, PhotoSize.Y)
+ SIZES.put(3, PhotoSize.Z)
+ SIZES.put(4, PhotoSize.W)
+ }
+ }
+
+ private val requestWritePermission = requestPermissionsAbs(
+ arrayOf(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ )
+ ) {
+ lazyPresenter { fireWriteExternalStoragePermissionResolved() }
+ }
+
+ private var mViewPager: ViewPager2? = null
+ private var mContentRoot: RelativeLayout? = null
+ private var mButtonWithUser: CircleCounterButton? = null
+ private var mButtonLike: CircleCounterButton? = null
+ private var mButtonComments: CircleCounterButton? = null
+ private var buttonShare: CircleCounterButton? = null
+ private var mLoadingProgressBar: RLottieImageView? = null
+ private var mLoadingProgressBarDispose = Disposable.disposed()
+ private var mLoadingProgressBarLoaded = false
+ private var mToolbar: Toolbar? = null
+ private var mButtonsRoot: View? = null
+ private var mPreviewsRecycler: RecyclerView? = null
+ private var mButtonRestore: MaterialButton? = null
+ private var mPagerAdapter: Adapter? = null
+ private var mCanSaveYourself = false
+ private var mCanDelete = false
+ private val bShowPhotosLine = Settings.get().other().isShow_photos_line
+ private val mAdapterRecycler = ImageListAdapter()
+
+ @LayoutRes
+ override fun getNoMainContentView(): Int {
+ return R.layout.activity_photo_pager
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Slidr.attach(
+ this,
+ SlidrConfig.Builder().setAlphaForView(false).fromUnColoredToColoredStatusBar(true)
+ .position(SlidrPosition.VERTICAL)
+ .listener(object : SlidrListener {
+ override fun onSlideStateChanged(state: Int) {
+
+ }
+
+ @SuppressLint("Range")
+ override fun onSlideChange(percent: Float) {
+ var tmp = 1f - percent
+ tmp *= 4
+ tmp = Utils.clamp(1f - tmp, 0f, 1f)
+ if (Utils.hasOreo()) {
+ mContentRoot?.setBackgroundColor(Color.argb(tmp, 0f, 0f, 0f))
+ } else {
+ mContentRoot?.setBackgroundColor(
+ Color.argb(
+ (tmp * 255).toInt(),
+ 0,
+ 0,
+ 0
+ )
+ )
+ }
+ mButtonsRoot?.alpha = tmp
+ mToolbar?.alpha = tmp
+ mPreviewsRecycler?.alpha = tmp
+ mViewPager?.alpha = Utils.clamp(percent, 0f, 1f)
+ }
+
+ override fun onSlideOpened() {
+
+ }
+
+ override fun onSlideClosed(): Boolean {
+ presenter?.close()
+ return true
+ }
+
+ }).build()
+ )
+ mContentRoot = findViewById(R.id.photo_pager_root)
+ mLoadingProgressBar = findViewById(R.id.loading_progress_bar)
+ mButtonRestore = findViewById(R.id.button_restore)
+ mButtonsRoot = findViewById(R.id.buttons)
+ mPreviewsRecycler = findViewById(R.id.previews_photos)
+ mToolbar = findViewById(R.id.toolbar)
+ setSupportActionBar(mToolbar)
+ mViewPager = findViewById(R.id.view_pager)
+ mViewPager?.offscreenPageLimit = 1
+ mViewPager?.setPageTransformer(
+ Utils.createPageTransform(
+ Settings.get().main().viewpager_page_transform
+ )
+ )
+ mViewPager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+ override fun onPageSelected(position: Int) {
+ super.onPageSelected(position)
+ presenter?.firePageSelected(position)
+
+ if (bShowPhotosLine) {
+ val currentSelected = mAdapterRecycler.getSelectedItem()
+ if (currentSelected != position) {
+ mAdapterRecycler.selectPosition(position)
+ if (currentSelected < position) {
+ mPreviewsRecycler?.scrollToPosition(position)
+ } else {
+ if (position == 0) {
+ mPreviewsRecycler?.scrollToPosition(position)
+ } else
+ mPreviewsRecycler?.scrollToPosition(position)
+ }
+ }
+ }
+ }
+ })
+ mButtonLike = findViewById(R.id.like_button)
+ mButtonLike?.setOnClickListener { presenter?.fireLikeClick() }
+ mButtonLike?.setOnLongClickListener {
+ presenter?.fireLikeLongClick()
+ false
+ }
+ mButtonWithUser = findViewById(R.id.with_user_button)
+ mButtonWithUser?.setOnClickListener { presenter?.fireWithUserClick() }
+ mButtonComments = findViewById(R.id.comments_button)
+ mButtonComments?.setOnClickListener { presenter?.fireCommentsButtonClick() }
+ buttonShare = findViewById(R.id.share_button)
+ buttonShare?.setOnClickListener { presenter?.fireShareButtonClick() }
+ mButtonRestore?.setOnClickListener { presenter?.fireButtonRestoreClick() }
+
+ if (bShowPhotosLine) {
+ mPreviewsRecycler?.layoutManager =
+ LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
+ mAdapterRecycler.setListener(object : ImageListAdapter.OnRecyclerImageClickListener {
+ override fun onRecyclerImageClick(index: Int) {
+ mViewPager?.currentItem = index
+ }
+ })
+ mPreviewsRecycler?.adapter = mAdapterRecycler
+ } else {
+ mPreviewsRecycler?.visibility = View.GONE
+ }
+
+ addMenuProvider(this, this)
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ presenter?.close()
+ }
+ })
+ }
+
+ override fun openPlace(place: Place) {
+ val args = place.safeArguments()
+ when (place.type) {
+ Place.PLAYER -> {
+ val player = supportFragmentManager.findFragmentByTag("audio_player")
+ if (player is AudioPlayerFragment) player.dismiss()
+ AudioPlayerFragment.newInstance(args).show(supportFragmentManager, "audio_player")
+ }
+ else -> {
+ val intent = Intent(this, SwipebleActivity::class.java)
+ intent.action = MainActivity.ACTION_OPEN_PLACE
+ intent.putExtra(Extra.PLACE, place)
+ SwipebleActivity.start(this, intent)
+ }
+ }
+ }
+
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.vkphoto_menu, menu)
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ when (menuItem.itemId) {
+ R.id.photo_size -> onPhotoSizeClicked()
+ R.id.save_on_drive -> {
+ presenter?.fireSaveOnDriveClick()
+ return true
+ }
+ R.id.save_yourself -> presenter?.fireSaveYourselfClick()
+ R.id.action_delete -> presenter?.fireDeleteClick()
+ R.id.info -> presenter?.fireInfoButtonClick()
+ R.id.detect_qr -> presenter?.fireDetectQRClick(this)
+ }
+ return false
+ }
+
+ override fun goToLikesList(accountId: Int, ownerId: Int, photoId: Int) {
+ PlaceFactory.getLikesCopiesPlace(
+ accountId,
+ "photo",
+ ownerId,
+ photoId,
+ ILikesInteractor.FILTER_LIKES
+ ).tryOpenWith(this)
+ }
+
+ override fun onPrepareMenu(menu: Menu) {
+ if (!Utils.isHiddenCurrent) {
+ menu.findItem(R.id.save_yourself).isVisible = mCanSaveYourself
+ menu.findItem(R.id.action_delete).isVisible = mCanDelete
+ } else {
+ menu.findItem(R.id.save_yourself).isVisible = false
+ menu.findItem(R.id.action_delete).isVisible = false
+ }
+ val imageSize = photoSizeFromPrefs
+ menu.findItem(R.id.photo_size).title = getTitleForPhotoSize(imageSize)
+ }
+
+ private fun onPhotoSizeClicked() {
+ val view = findViewById(R.id.photo_size)
+ val current = photoSizeFromPrefs
+ val popupMenu = PopupMenu(this, view)
+ for (i in 0 until SIZES.size()) {
+ val key = SIZES.keyAt(i)
+ val value = SIZES[key]
+ addPhotoSizeToMenu(popupMenu, key, value, current)
+ }
+ popupMenu.menu.setGroupCheckable(0, true, true)
+ popupMenu.setOnMenuItemClickListener { item: MenuItem ->
+ val key = item.itemId
+ Settings.get()
+ .main()
+ .setPrefDisplayImageSize(SIZES[key])
+ invalidateOptionsMenu()
+ true
+ }
+ popupMenu.show()
+ }
+
+ override fun getPresenterFactory(saveInstanceState: Bundle?): IPresenterFactory =
+ object : IPresenterFactory {
+ override fun create(): PhotoPagerPresenter {
+ val placeType = requireArguments().getInt(Extra.PLACE_TYPE)
+ val aid = requireArguments().getInt(Extra.ACCOUNT_ID)
+ when (placeType) {
+ Place.SIMPLE_PHOTO_GALLERY -> {
+ val index = requireArguments().getInt(Extra.INDEX)
+ val needUpdate = requireArguments().getBoolean(EXTRA_NEED_UPDATE)
+ val photos: ArrayList =
+ requireArguments().getParcelableArrayListCompat(EXTRA_PHOTOS)!!
+ return SimplePhotoPresenter(
+ photos,
+ index,
+ needUpdate,
+ aid,
+ this@PhotoPagerActivity,
+ saveInstanceState
+ )
+ }
+ Place.VK_PHOTO_ALBUM_GALLERY_SAVED -> {
+ val indexx = requireArguments().getInt(Extra.INDEX)
+ val ownerId = requireArguments().getInt(Extra.OWNER_ID)
+ val albumId = requireArguments().getInt(Extra.ALBUM_ID)
+ val readOnly = requireArguments().getBoolean(Extra.READONLY)
+ val invert = requireArguments().getBoolean(Extra.INVERT)
+ val source: TmpSource =
+ requireArguments().getParcelableCompat(Extra.SOURCE)!!
+ return PhotoAlbumPagerPresenter(
+ indexx,
+ aid,
+ ownerId,
+ albumId,
+ source,
+ readOnly,
+ invert,
+ this@PhotoPagerActivity,
+ saveInstanceState
+ )
+ }
+ Place.VK_PHOTO_ALBUM_GALLERY, Place.VK_PHOTO_ALBUM_GALLERY_NATIVE -> {
+ val indexx = requireArguments().getInt(Extra.INDEX)
+ val ownerId = requireArguments().getInt(Extra.OWNER_ID)
+ val albumId = requireArguments().getInt(Extra.ALBUM_ID)
+ val readOnly = requireArguments().getBoolean(Extra.READONLY)
+ val invert = requireArguments().getBoolean(Extra.INVERT)
+ val photos_album: ArrayList =
+ if (FenrirNative.isNativeLoaded && Settings.get()
+ .other().isNative_parcel_photo
+ ) ParcelNative.loadParcelableArrayList(
+ requireArguments().getLong(
+ EXTRA_PHOTOS
+ ), Photo.NativeCreator, ParcelFlags.MUTABLE_LIST
+ )!! else requireArguments().getParcelableArrayListCompat(EXTRA_PHOTOS)!!
+ if (FenrirNative.isNativeLoaded && Settings.get()
+ .other().isNative_parcel_photo
+ ) {
+ requireArguments().putLong(EXTRA_PHOTOS, 0)
+ }
+ return PhotoAlbumPagerPresenter(
+ indexx,
+ aid,
+ ownerId,
+ albumId,
+ photos_album,
+ readOnly,
+ invert,
+ this@PhotoPagerActivity,
+ saveInstanceState
+ )
+ }
+ Place.FAVE_PHOTOS_GALLERY -> {
+ val findex = requireArguments().getInt(Extra.INDEX)
+ val favePhotos: ArrayList =
+ requireArguments().getParcelableArrayListCompat(EXTRA_PHOTOS)!!
+ return FavePhotoPagerPresenter(
+ favePhotos,
+ findex,
+ aid,
+ this@PhotoPagerActivity,
+ saveInstanceState
+ )
+ }
+ Place.VK_PHOTO_TMP_SOURCE -> {
+ if (!FenrirNative.isNativeLoaded || !Settings.get()
+ .other().isNative_parcel_photo
+ ) {
+ val source: TmpSource =
+ requireArguments().getParcelableCompat(Extra.SOURCE)!!
+ return TmpGalleryPagerPresenter(
+ aid,
+ source,
+ requireArguments().getInt(Extra.INDEX),
+ this@PhotoPagerActivity,
+ saveInstanceState
+ )
+ } else {
+ val source: Long = requireArguments().getLong(Extra.SOURCE)
+ requireArguments().putLong(Extra.SOURCE, 0)
+ return TmpGalleryPagerPresenter(
+ aid,
+ source,
+ requireArguments().getInt(Extra.INDEX),
+ this@PhotoPagerActivity,
+ saveInstanceState
+ )
+ }
+ }
+ }
+ throw UnsupportedOperationException()
+ }
+ }
+
+ override fun setupLikeButton(visible: Boolean, like: Boolean, likes: Int) {
+ mButtonLike?.visibility = if (visible) View.VISIBLE else View.GONE
+ mButtonLike?.isActive = like
+ mButtonLike?.count = likes
+ mButtonLike?.setIcon(if (like) R.drawable.heart_filled else R.drawable.heart)
+ }
+
+ override fun setupWithUserButton(users: Int) {
+ mButtonWithUser?.visibility = if (users > 0) View.VISIBLE else View.GONE
+ mButtonWithUser?.count = users
+ }
+
+ override fun setupShareButton(visible: Boolean, reposts: Int) {
+ buttonShare?.visibility = if (visible) View.VISIBLE else View.GONE
+ buttonShare?.count = reposts
+ }
+
+ override fun setupCommentsButton(visible: Boolean, count: Int) {
+ mButtonComments?.visibility = if (visible) View.VISIBLE else View.GONE
+ mButtonComments?.count = count
+ }
+
+ override fun displayPhotos(photos: List, initialIndex: Int) {
+ if (bShowPhotosLine) {
+ if (photos.size <= 1) {
+ mAdapterRecycler.setData(Collections.emptyList())
+ mAdapterRecycler.notifyDataSetChanged()
+ } else {
+ mAdapterRecycler.setData(photos)
+ mAdapterRecycler.notifyDataSetChanged()
+ mAdapterRecycler.selectPosition(initialIndex)
+ }
+ }
+ mPagerAdapter = Adapter(photos)
+ mViewPager?.adapter = mPagerAdapter
+ mViewPager?.setCurrentItem(initialIndex, false)
+ }
+
+ override fun sharePhoto(accountId: Int, photo: Photo) {
+ val items = arrayOf(
+ getString(R.string.share_link),
+ getString(R.string.repost_send_message),
+ getString(R.string.repost_to_wall)
+ )
+ MaterialAlertDialogBuilder(this)
+ .setItems(items) { _: DialogInterface?, i: Int ->
+ when (i) {
+ 0 -> Utils.shareLink(this, photo.generateWebLink(), photo.text)
+ 1 -> SendAttachmentsActivity.startForSendAttachments(
+ this,
+ accountId,
+ photo
+ )
+ 2 -> presenter?.firePostToMyWallClick()
+ }
+ }
+ .setCancelable(true)
+ .setTitle(R.string.share_photo_title)
+ .show()
+ }
+
+ override fun postToMyWall(photo: Photo, accountId: Int) {
+ PlaceUtil.goToPostCreation(
+ this,
+ accountId,
+ accountId,
+ EditingPostType.TEMP,
+ listOf(photo)
+ )
+ }
+
+ override fun requestWriteToExternalStoragePermission() {
+ requestWritePermission.launch()
+ }
+
+ override fun setButtonRestoreVisible(visible: Boolean) {
+ mButtonRestore?.visibility = if (visible) View.VISIBLE else View.GONE
+ }
+
+ override fun setupOptionMenu(canSaveYourself: Boolean, canDelete: Boolean) {
+ mCanSaveYourself = canSaveYourself
+ mCanDelete = canDelete
+ this.invalidateOptionsMenu()
+ }
+
+ override fun goToComments(accountId: Int, commented: Commented) {
+ PlaceFactory.getCommentsPlace(accountId, commented, null).tryOpenWith(this)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ mLoadingProgressBarDispose.dispose()
+ }
+
+ override fun displayPhotoListLoading(loading: Boolean) {
+ mLoadingProgressBarDispose.dispose()
+ if (loading) {
+ mLoadingProgressBarDispose = Completable.create {
+ it.onComplete()
+ }.delay(300, TimeUnit.MILLISECONDS).fromIOToMain().subscribe({
+ mLoadingProgressBarLoaded = true
+ mLoadingProgressBar?.visibility = View.VISIBLE
+ mLoadingProgressBar?.fromRes(
+ dev.ragnarok.fenrir_common.R.raw.loading,
+ Utils.dp(100F),
+ Utils.dp(100F),
+ intArrayOf(
+ 0x000000,
+ Color.WHITE,
+ 0x777777,
+ Color.WHITE
+ )
+ )
+ mLoadingProgressBar?.playAnimation()
+ }, RxUtils.ignore())
+ } else if (mLoadingProgressBarLoaded) {
+ mLoadingProgressBarLoaded = false
+ mLoadingProgressBar?.visibility = View.GONE
+ mLoadingProgressBar?.clearAnimationDrawable()
+ }
+ }
+
+ override fun setButtonsBarVisible(visible: Boolean) {
+ mButtonsRoot?.visibility = if (visible) View.VISIBLE else View.GONE
+ mPreviewsRecycler?.visibility = if (visible && bShowPhotosLine) View.VISIBLE else View.GONE
+ }
+
+ override fun setToolbarVisible(visible: Boolean) {
+ mToolbar?.visibility = if (visible) View.VISIBLE else View.GONE
+ }
+
+ override fun rebindPhotoAt(position: Int) {
+ mPagerAdapter?.notifyItemChanged(position)
+ if (bShowPhotosLine && mAdapterRecycler.getSize() > 1) {
+ mAdapterRecycler.notifyItemChanged(position)
+ }
+ }
+
+ override fun closeOnly() {
+ finish()
+ overridePendingTransition(0, 0)
+ }
+
+ override fun returnInfo(position: Int, parcelNativePtr: Long) {
+ setResult(
+ RESULT_OK,
+ Intent().putExtra(Extra.PTR, parcelNativePtr).putExtra(Extra.POSITION, position)
+ )
+ finish()
+ overridePendingTransition(0, 0)
+ }
+
+ override fun returnOnlyPos(position: Int) {
+ setResult(
+ RESULT_OK,
+ Intent().putExtra(Extra.POSITION, position)
+ )
+ finish()
+ overridePendingTransition(0, 0)
+ }
+
+ override fun hideMenu(hide: Boolean) {}
+
+ override fun openMenu(open: Boolean) {}
+
+ @Suppress("DEPRECATION")
+ override fun setStatusbarColored(colored: Boolean, invertIcons: Boolean) {
+ val statusbarNonColored = CurrentTheme.getStatusBarNonColored(this)
+ val statusbarColored = CurrentTheme.getStatusBarColor(this)
+ val w = window
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.statusBarColor = if (colored) statusbarColored else statusbarNonColored
+ @ColorInt val navigationColor =
+ if (colored) CurrentTheme.getNavigationBarColor(this) else Color.BLACK
+ w.navigationBarColor = navigationColor
+ if (Utils.hasMarshmallow()) {
+ var flags = window.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ }
+ window.decorView.systemUiVisibility = flags
+ }
+ if (Utils.hasOreo()) {
+ var flags = window.decorView.systemUiVisibility
+ if (invertIcons) {
+ flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ w.decorView.systemUiVisibility = flags
+ w.navigationBarColor = Color.WHITE
+ } else {
+ flags = flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ w.decorView.systemUiVisibility = flags
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ ActivityFeatures.Builder()
+ .begin()
+ .setHideNavigationMenu(true)
+ .setBarsColored(colored = false, invertIcons = false)
+ .build()
+ .apply(this)
+ }
+
+ @get:PhotoSize
+ val photoSizeFromPrefs: Int
+ get() = Settings.get()
+ .main()
+ .getPrefDisplayImageSize(DEFAULT_PHOTO_SIZE)
+
+ private inner class PhotoViewHolder(view: View) : RecyclerView.ViewHolder(view), Callback {
+ val reload: FloatingActionButton
+ private val mPicassoLoadCallback: WeakPicassoLoadCallback
+ val photo: TouchImageView
+ val progress: RLottieImageView
+ var animationDispose: Disposable = Disposable.disposed()
+ private var mAnimationLoaded = false
+ private var mLoadingNow = false
+ fun bindTo(photo_image: Photo) {
+ photo.resetZoom()
+ val size: Int = photoSizeFromPrefs
+ val url = photo_image.getUrlForSize(size, true)
+ reload.setOnClickListener {
+ reload.visibility = View.INVISIBLE
+ if (url.nonNullNoEmpty()) {
+ loadImage(url)
+ } else PicassoInstance.with().cancelRequest(photo)
+ }
+ if (url.nonNullNoEmpty()) {
+ loadImage(url)
+ } else {
+ PicassoInstance.with().cancelRequest(photo)
+ createCustomToast(this@PhotoPagerActivity).showToastError(R.string.empty_url)
+ }
+ }
+
+ private fun resolveProgressVisibility(forceStop: Boolean) {
+ animationDispose.dispose()
+ if (mAnimationLoaded && !mLoadingNow && !forceStop) {
+ mAnimationLoaded = false
+ val k = ObjectAnimator.ofFloat(progress, View.ALPHA, 0.0f).setDuration(1000)
+ k.addListener(object : StubAnimatorListener() {
+ override fun onAnimationEnd(animation: Animator) {
+ progress.clearAnimationDrawable()
+ progress.visibility = View.GONE
+ progress.alpha = 1f
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ progress.clearAnimationDrawable()
+ progress.visibility = View.GONE
+ progress.alpha = 1f
+ }
+ })
+ k.start()
+ } else if (mAnimationLoaded && !mLoadingNow) {
+ mAnimationLoaded = false
+ progress.clearAnimationDrawable()
+ progress.visibility = View.GONE
+ } else if (mLoadingNow) {
+ animationDispose = Completable.create {
+ it.onComplete()
+ }.delay(300, TimeUnit.MILLISECONDS).fromIOToMain().subscribe({
+ mAnimationLoaded = true
+ progress.visibility = View.VISIBLE
+ progress.fromRes(
+ dev.ragnarok.fenrir_common.R.raw.loading,
+ Utils.dp(100F),
+ Utils.dp(100F),
+ intArrayOf(
+ 0x000000,
+ CurrentTheme.getColorPrimary(this@PhotoPagerActivity),
+ 0x777777,
+ CurrentTheme.getColorSecondary(this@PhotoPagerActivity)
+ )
+ )
+ progress.playAnimation()
+ }, RxUtils.ignore())
+ }
+ }
+
+ private fun loadImage(url: String) {
+ mLoadingNow = true
+ resolveProgressVisibility(true)
+ PicassoInstance.with()
+ .load(url)
+ .into(photo, mPicassoLoadCallback)
+ }
+
+ @IdRes
+ private fun idOfImageView(): Int {
+ return R.id.image_view
+ }
+
+ @IdRes
+ private fun idOfProgressBar(): Int {
+ return R.id.progress_bar
+ }
+
+ override fun onSuccess() {
+ mLoadingNow = false
+ resolveProgressVisibility(false)
+ reload.visibility = View.INVISIBLE
+ }
+
+ override fun onError(t: Throwable) {
+ mLoadingNow = false
+ resolveProgressVisibility(true)
+ reload.visibility = View.VISIBLE
+ }
+
+ init {
+ photo = view.findViewById(idOfImageView())
+ photo.maxZoom = 8f
+ photo.doubleTapScale = 2f
+ photo.doubleTapMaxZoom = 4f
+ progress = view.findViewById(idOfProgressBar())
+ reload = view.findViewById(R.id.goto_button)
+ mPicassoLoadCallback = WeakPicassoLoadCallback(this)
+ photo.setOnClickListener { presenter?.firePhotoTap() }
+ }
+ }
+
+ private inner class Adapter(val mPhotos: List) :
+ RecyclerView.Adapter() {
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onCreateViewHolder(container: ViewGroup, viewType: Int): PhotoViewHolder {
+ val ret = PhotoViewHolder(
+ LayoutInflater.from(container.context)
+ .inflate(R.layout.content_photo_page, container, false)
+ )
+ ret.photo.setOnLongClickListener {
+ if (Settings.get().other().isDownload_photo_tap) {
+ presenter?.fireSaveOnDriveClick()
+ } else if (ret.photo.drawable is Rotatable) {
+ var rot = (ret.photo.drawable as Rotatable).getRotation() + 45
+ if (rot >= 360f) {
+ rot = 0f
+ }
+ (ret.photo.drawable as Rotatable).rotate(rot)
+ ret.photo.fitImageToView()
+ ret.photo.invalidate()
+ }
+ true
+ }
+ ret.photo.setOnTouchListener { view: View, event: MotionEvent ->
+ if (event.pointerCount >= 2 || view.canScrollHorizontally(1) && view.canScrollHorizontally(
+ -1
+ )
+ ) {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
+ container.requestDisallowInterceptTouchEvent(true)
+ return@setOnTouchListener false
+ }
+ MotionEvent.ACTION_UP -> {
+ container.requestDisallowInterceptTouchEvent(false)
+ return@setOnTouchListener true
+ }
+ }
+ }
+ true
+ }
+ return ret
+ }
+
+ override fun onViewDetachedFromWindow(holder: PhotoViewHolder) {
+ super.onViewDetachedFromWindow(holder)
+ PicassoInstance.with().cancelRequest(holder.photo)
+ }
+
+ override fun onBindViewHolder(holder: PhotoViewHolder, position: Int) {
+ val photo = mPhotos[position]
+ holder.bindTo(photo)
+ }
+
+ override fun getItemCount(): Int {
+ return mPhotos.size
+ }
+ }
+}
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/PhotoPagerPresenter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/PhotoPagerPresenter.kt
new file mode 100644
index 000000000..913b04569
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/PhotoPagerPresenter.kt
@@ -0,0 +1,667 @@
+package dev.ragnarok.fenrir.activity.photopager
+
+import android.app.Activity
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.DialogInterface
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.squareup.picasso3.BitmapTarget
+import com.squareup.picasso3.Picasso.LoadedFrom
+import dev.ragnarok.fenrir.App.Companion.instance
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.qr.CameraScanActivity
+import dev.ragnarok.fenrir.domain.IOwnersRepository
+import dev.ragnarok.fenrir.domain.IPhotosInteractor
+import dev.ragnarok.fenrir.domain.InteractorFactory
+import dev.ragnarok.fenrir.domain.Repository.owners
+import dev.ragnarok.fenrir.fragment.base.AccountDependencyPresenter
+import dev.ragnarok.fenrir.fromIOToMain
+import dev.ragnarok.fenrir.link.LinkHelper
+import dev.ragnarok.fenrir.model.*
+import dev.ragnarok.fenrir.nonNullNoEmpty
+import dev.ragnarok.fenrir.picasso.PicassoInstance.Companion.with
+import dev.ragnarok.fenrir.place.PlaceFactory
+import dev.ragnarok.fenrir.push.OwnerInfo
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.util.AppPerms
+import dev.ragnarok.fenrir.util.AppTextUtils
+import dev.ragnarok.fenrir.util.AssertUtils
+import dev.ragnarok.fenrir.util.DownloadWorkUtils.doDownloadPhoto
+import dev.ragnarok.fenrir.util.DownloadWorkUtils.fixStart
+import dev.ragnarok.fenrir.util.DownloadWorkUtils.makeLegalFilename
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.toast.CustomToast.Companion.createCustomToast
+import io.reactivex.rxjava3.core.Completable
+import java.io.File
+import java.util.*
+import kotlin.math.abs
+
+open class PhotoPagerPresenter internal constructor(
+ protected var mPhotos: ArrayList,
+ accountId: Int,
+ private val read_only: Boolean,
+ private val context: Context,
+ savedInstanceState: Bundle?
+) : AccountDependencyPresenter(accountId, savedInstanceState) {
+ protected val photosInteractor: IPhotosInteractor = InteractorFactory.createPhotosInteractor()
+ protected var currentIndex = 0
+ private var mLoadingNow = false
+ private var mFullScreen = false
+ open fun close() {
+ view?.closeOnly()
+ }
+
+ fun changeLoadingNowState(loading: Boolean) {
+ mLoadingNow = loading
+ resolveLoadingView()
+ }
+
+ private fun resolveLoadingView() {
+ view?.displayPhotoListLoading(
+ mLoadingNow
+ )
+ }
+
+ fun refreshPagerView() {
+ view?.displayPhotos(
+ mPhotos,
+ currentIndex
+ )
+ }
+
+ override fun onViewHostAttached(view: IPhotoPagerView) {
+ super.onViewHostAttached(view)
+ resolveOptionMenu()
+ }
+
+ private fun resolveOptionMenu() {
+ view?.setupOptionMenu(
+ canSaveYourself(),
+ canDelete()
+ )
+ }
+
+ private fun canDelete(): Boolean {
+ return hasPhotos() && current.ownerId == accountId
+ }
+
+ private fun canSaveYourself(): Boolean {
+ return hasPhotos() && current.ownerId != accountId
+ }
+
+ override fun onGuiCreated(viewHost: IPhotoPagerView) {
+ super.onGuiCreated(viewHost)
+ view?.displayPhotos(
+ mPhotos,
+ currentIndex
+ )
+ refreshInfoViews(true)
+ resolveRestoreButtonVisibility()
+ resolveToolbarVisibility()
+ resolveButtonsBarVisible()
+ resolveLoadingView()
+ }
+
+ fun firePageSelected(position: Int) {
+ val old = currentIndex
+ changePageTo(position)
+ afterPageChangedFromUi(old, position)
+ }
+
+ protected open fun afterPageChangedFromUi(oldPage: Int, newPage: Int) {}
+ private fun changePageTo(position: Int) {
+ if (currentIndex == position) return
+ currentIndex = position
+ onPositionChanged()
+ }
+
+ private fun resolveLikeView() {
+ if (hasPhotos()) {
+ if (read_only) {
+ view?.setupLikeButton(
+ visible = false,
+ like = false,
+ likes = 0
+ )
+ return
+ }
+ val photo = current
+ view?.setupLikeButton(
+ true,
+ photo.isUserLikes,
+ photo.likesCount
+ )
+ }
+ }
+
+ private fun resolveWithUserView() {
+ if (hasPhotos()) {
+ val photo = current
+ view?.setupWithUserButton(photo.tagsCount)
+ }
+ }
+
+ private fun resolveShareView() {
+ if (hasPhotos()) {
+ val photo = current
+ view?.setupShareButton(!read_only, photo.repostsCount)
+ }
+ }
+
+ private fun resolveCommentsView() {
+ if (hasPhotos()) {
+ val photo = current
+ if (read_only) {
+ view?.setupCommentsButton(
+ false,
+ 0
+ )
+ return
+ }
+ //boolean visible = photo.isCanComment() || photo.getCommentsCount() > 0;
+ view?.setupCommentsButton(
+ true,
+ photo.commentsCount
+ )
+ }
+ }
+
+ fun count(): Int {
+ return mPhotos.size
+ }
+
+ private fun resolveToolbarTitleSubtitleView() {
+ if (!hasPhotos()) return
+ val title = context.getString(R.string.image_number, currentIndex + 1, count())
+ view?.setToolbarTitle(title)
+ view?.setToolbarSubtitle(current.text)
+ }
+
+ private val current: Photo
+ get() = mPhotos[currentIndex]
+
+ private fun onPositionChanged() {
+ refreshInfoViews(true)
+ resolveRestoreButtonVisibility()
+ resolveOptionMenu()
+ }
+
+ private fun showPhotoInfo(photo: Photo, album: PhotoAlbum?, bundle: IOwnersBundle?) {
+ if (photo.albumId == -311) {
+ return
+ }
+ val album_info =
+ if (album == null) context.getString(R.string.open_photo_album) else album.getDisplayTitle(
+ context
+ )
+ var user: String? =
+ if (photo.ownerId >= 0) context.getString(R.string.goto_user) else context.getString(R.string.goto_community)
+ if (bundle != null) {
+ user = bundle.getById(photo.ownerId).fullName
+ }
+ val buttons: MutableList = ArrayList(2)
+ buttons.add(FunctionSource(
+ album_info, R.drawable.photo_album
+ ) {
+ PlaceFactory.getVKPhotosAlbumPlace(
+ accountId,
+ photo.ownerId,
+ photo.albumId,
+ null,
+ photo.getObjectId()
+ )
+ .tryOpenWith(context)
+ })
+ buttons.add(FunctionSource(
+ user, R.drawable.person
+ ) {
+ PlaceFactory.getOwnerWallPlace(accountId, photo.ownerId, null).tryOpenWith(context)
+ })
+ val adapter = ButtonAdapter(context, buttons)
+ MaterialAlertDialogBuilder(context)
+ .setTitle(
+ context.getString(R.string.uploaded) + " " + AppTextUtils.getDateFromUnixTime(
+ photo.date
+ )
+ )
+ .setView(
+ Utils.createAlertRecycleFrame(
+ context,
+ adapter,
+ if (photo.text.isNullOrEmpty()) null else context.getString(R.string.description_hint) + ": " + photo.text,
+ accountId
+ )
+ )
+ .setPositiveButton(R.string.button_ok, null)
+ .setCancelable(true)
+ .show()
+ }
+
+ private fun getOwnerForPhoto(photo: Photo, album: PhotoAlbum?) {
+ appendDisposable(
+ owners.findBaseOwnersDataAsBundle(
+ accountId, setOf(photo.ownerId), IOwnersRepository.MODE_ANY
+ )
+ .fromIOToMain()
+ .subscribe({
+ showPhotoInfo(
+ photo,
+ album,
+ it
+ )
+ }) { showPhotoInfo(photo, album, null) })
+ }
+
+ fun fireInfoButtonClick() {
+ val photo = current
+ appendDisposable(photosInteractor.getAlbumById(accountId, photo.ownerId, photo.albumId)
+ .fromIOToMain()
+ .subscribe({
+ getOwnerForPhoto(
+ photo,
+ it
+ )
+ }) { getOwnerForPhoto(photo, null) })
+ }
+
+ fun fireShareButtonClick() {
+ val current = current
+ view?.sharePhoto(
+ accountId,
+ current
+ )
+ }
+
+ fun firePostToMyWallClick() {
+ val photo = current
+ view?.postToMyWall(
+ photo,
+ accountId
+ )
+ }
+
+ fun refreshInfoViews(need_update: Boolean) {
+ resolveToolbarTitleSubtitleView()
+ resolveLikeView()
+ resolveWithUserView()
+ resolveShareView()
+ resolveCommentsView()
+ resolveOptionMenu()
+ if (need_update && need_update_info() && hasPhotos()) {
+ val photo = current
+ if (photo.albumId != -311) {
+ appendDisposable(photosInteractor.getPhotosByIds(
+ accountId,
+ setOf(AccessIdPair(photo.getObjectId(), photo.ownerId, photo.accessKey))
+ )
+ .fromIOToMain()
+ .subscribe({
+ if (it[0].getObjectId() == photo.getObjectId()) {
+ val ne = it[0]
+ if (ne.accessKey == null) {
+ ne.setAccessKey(photo.accessKey)
+ }
+ mPhotos[currentIndex] = ne
+ refreshInfoViews(false)
+ }
+ }) { })
+ }
+ }
+ }
+
+ protected open fun need_update_info(): Boolean {
+ return false
+ }
+
+ fun fireLikeClick() {
+ addOrRemoveLike()
+ }
+
+ private fun addOrRemoveLike() {
+ if (Settings.get().other().isDisable_likes || Utils.isHiddenAccount(
+ accountId
+ )
+ ) {
+ return
+ }
+ val photo = current
+ val ownerId = photo.ownerId
+ val photoId = photo.getObjectId()
+ val add = !photo.isUserLikes
+ appendDisposable(photosInteractor.like(accountId, ownerId, photoId, add, photo.accessKey)
+ .fromIOToMain()
+ .subscribe({
+ interceptLike(
+ ownerId,
+ photoId,
+ it,
+ add
+ )
+ }) { t ->
+ view?.let {
+ showError(it, Utils.getCauseIfRuntime(t))
+ }
+ })
+ }
+
+ private fun onDeleteOrRestoreResult(photoId: Int, ownerId: Int, deleted: Boolean) {
+ val index = Utils.findIndexById(mPhotos, photoId, ownerId)
+ if (index != -1) {
+ val photo = mPhotos[index]
+ photo.setDeleted(deleted)
+ if (currentIndex == index) {
+ resolveRestoreButtonVisibility()
+ }
+ }
+ }
+
+ private fun interceptLike(ownerId: Int, photoId: Int, count: Int, userLikes: Boolean) {
+ for (photo in mPhotos) {
+ if (photo.getObjectId() == photoId && photo.ownerId == ownerId) {
+ photo.setLikesCount(count)
+ photo.setUserLikes(userLikes)
+ resolveLikeView()
+ break
+ }
+ }
+ }
+
+ fun fireSaveOnDriveClick() {
+ if (!AppPerms.hasReadWriteStoragePermission(instance)) {
+ view?.requestWriteToExternalStoragePermission()
+ return
+ }
+ doSaveOnDrive()
+ }
+
+ private fun doSaveOnDrive() {
+ val dir = File(Settings.get().other().photoDir)
+ if (!dir.isDirectory) {
+ val created = dir.mkdirs()
+ if (!created) {
+ view?.showError("Can't create directory $dir")
+ return
+ }
+ } else dir.setLastModified(Calendar.getInstance().time.time)
+ val photo = current
+ if (photo.albumId == -311) {
+ var path = photo.text
+ val ndx = path?.indexOf('/') ?: -1
+ if (ndx != -1) {
+ path = path?.substring(0, ndx)
+ }
+ downloadResult(fixStart(path), dir, photo)
+ } else {
+ appendDisposable(OwnerInfo.getRx(context, accountId, photo.ownerId)
+ .fromIOToMain()
+ .subscribe({
+ downloadResult(
+ makeLegalFilename(
+ fixStart(it.owner.fullName) ?: ("id" + photo.ownerId),
+ null
+ ), dir, photo
+ )
+ }) { downloadResult(null, dir, photo) })
+ }
+ }
+
+ private fun transform_owner(owner_id: Int): String {
+ return if (owner_id < 0) "club" + abs(owner_id) else "id$owner_id"
+ }
+
+ private fun downloadResult(Prefix: String?, dirL: File, photo: Photo) {
+ var dir = dirL
+ if (Prefix != null && Settings.get().other().isPhoto_to_user_dir) {
+ val dir_final = File(dir.absolutePath + "/" + Prefix)
+ if (!dir_final.isDirectory) {
+ val created = dir_final.mkdirs()
+ if (!created) {
+ view?.showError("Can't create directory $dir_final")
+ return
+ }
+ } else dir_final.setLastModified(Calendar.getInstance().time.time)
+ dir = dir_final
+ }
+ val url = photo.getUrlForSize(PhotoSize.W, true)
+ if (url != null) {
+ doDownloadPhoto(
+ context,
+ url,
+ dir.absolutePath,
+ (if (Prefix != null) Prefix + "_" else "") + transform_owner(photo.ownerId) + "_" + photo.getObjectId()
+ )
+ }
+ }
+
+ fun fireSaveYourselfClick() {
+ val photo = current
+ if (photo.albumId == -311) {
+ return
+ }
+ appendDisposable(photosInteractor.copy(
+ accountId,
+ photo.ownerId,
+ photo.getObjectId(),
+ photo.accessKey
+ )
+ .fromIOToMain()
+ .subscribe({ onPhotoCopied() }) { t ->
+ view?.let {
+ showError(
+ it,
+ Utils.getCauseIfRuntime(t)
+ )
+ }
+ })
+ }
+
+ fun fireDetectQRClick(context: Activity) {
+ with().load(current.getUrlForSize(PhotoSize.W, false))
+ .into(object : BitmapTarget {
+ override fun onBitmapLoaded(bitmap: Bitmap, from: LoadedFrom) {
+ val data = CameraScanActivity.decodeFromBitmap(bitmap)
+ MaterialAlertDialogBuilder(context)
+ .setIcon(R.drawable.qr_code)
+ .setMessage(data)
+ .setTitle(getString(R.string.scan_qr))
+ .setPositiveButton(R.string.open) { _: DialogInterface?, _: Int ->
+ LinkHelper.openUrl(
+ context,
+ accountId,
+ data, false
+ )
+ }
+ .setNeutralButton(R.string.copy_text) { _: DialogInterface?, _: Int ->
+ val clipboard = context.getSystemService(
+ Context.CLIPBOARD_SERVICE
+ ) as ClipboardManager?
+ val clip = ClipData.newPlainText("response", data)
+ clipboard?.setPrimaryClip(clip)
+ createCustomToast(context).showToast(R.string.copied_to_clipboard)
+ }
+ .setCancelable(true)
+ .show()
+ }
+
+ override fun onBitmapFailed(e: Exception, errorDrawable: Drawable?) {
+ createCustomToast(context).showToastError(e.localizedMessage)
+ }
+
+ override fun onPrepareLoad(placeHolderDrawable: Drawable?) {}
+ })
+ }
+
+ private fun onPhotoCopied() {
+ view?.customToast?.showToastSuccessBottom(
+ R.string.photo_saved_yourself
+ )
+ }
+
+ fun fireDeleteClick() {
+ delete()
+ }
+
+ fun fireWriteExternalStoragePermissionResolved() {
+ if (AppPerms.hasReadWriteStoragePermission(instance)) {
+ doSaveOnDrive()
+ }
+ }
+
+ fun fireButtonRestoreClick() {
+ restore()
+ }
+
+ private fun resolveRestoreButtonVisibility() {
+ view?.setButtonRestoreVisible(
+ hasPhotos() && current.isDeleted
+ )
+ }
+
+ private fun restore() {
+ deleteOrRestore(false)
+ }
+
+ private fun deleteOrRestore(detele: Boolean) {
+ val photo = current
+ if (photo.albumId == -311) {
+ return
+ }
+ val photoId = photo.getObjectId()
+ val ownerId = photo.ownerId
+ val completable: Completable = if (detele) {
+ photosInteractor.deletePhoto(accountId, ownerId, photoId)
+ } else {
+ photosInteractor.restorePhoto(accountId, ownerId, photoId)
+ }
+ appendDisposable(completable.fromIOToMain()
+ .subscribe({ onDeleteOrRestoreResult(photoId, ownerId, detele) }) { t ->
+ view?.let {
+ showError(
+ it,
+ Utils.getCauseIfRuntime(t)
+ )
+ }
+ })
+ }
+
+ private fun delete() {
+ deleteOrRestore(true)
+ }
+
+ fun fireCommentsButtonClick() {
+ val photo = current
+ view?.goToComments(
+ accountId,
+ Commented.from(photo)
+ )
+ }
+
+ fun fireWithUserClick() {
+ val photo = current
+ appendDisposable(
+ InteractorFactory.createPhotosInteractor()
+ .getTags(accountId, photo.ownerId, photo.getObjectId(), photo.accessKey)
+ .fromIOToMain()
+ .subscribe({
+ val buttons: MutableList = ArrayList(it.size)
+ for (i in it) {
+ if (i.user_id != 0) {
+ buttons.add(FunctionSource(i.tagged_name, R.drawable.person) {
+ PlaceFactory.getOwnerWallPlace(
+ accountId, i.user_id, null
+ ).tryOpenWith(context)
+ })
+ } else {
+ buttons.add(FunctionSource(i.tagged_name, R.drawable.pencil) {})
+ }
+ }
+ val adapter = ButtonAdapter(context, buttons)
+ MaterialAlertDialogBuilder(context)
+ .setTitle(R.string.has_tags)
+ .setPositiveButton(R.string.button_ok, null)
+ .setCancelable(true)
+ .setView(Utils.createAlertRecycleFrame(context, adapter, null, accountId))
+ .show()
+ }) { throwable ->
+ view?.let {
+ showError(
+ it,
+ throwable
+ )
+ }
+ })
+ }
+
+ private fun hasPhotos(): Boolean {
+ return mPhotos.nonNullNoEmpty()
+ }
+
+ fun firePhotoTap() {
+ if (!hasPhotos()) return
+ mFullScreen = !mFullScreen
+ resolveToolbarVisibility()
+ resolveButtonsBarVisible()
+ }
+
+ fun resolveButtonsBarVisible() {
+ view?.setButtonsBarVisible(
+ hasPhotos() && !mFullScreen
+ )
+ }
+
+ fun resolveToolbarVisibility() {
+ view?.setToolbarVisible(hasPhotos() && !mFullScreen)
+ }
+
+ fun fireLikeLongClick() {
+ if (!hasPhotos()) return
+ val photo = current
+ view?.goToLikesList(
+ accountId,
+ photo.ownerId,
+ photo.getObjectId()
+ )
+ }
+
+ private class ButtonAdapter(
+ private val context: Context,
+ private val items: List
+ ) : RecyclerView.Adapter() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ButtonHolder {
+ return ButtonHolder(
+ LayoutInflater.from(
+ context
+ ).inflate(R.layout.item_button, parent, false)
+ )
+ }
+
+ override fun onBindViewHolder(holder: ButtonHolder, position: Int) {
+ val source = items[position]
+ holder.button.text = source.getTitle(context)
+ holder.button.setIconResource(source.getIcon())
+ holder.button.setOnClickListener { source.Do() }
+ }
+
+ override fun getItemCount(): Int {
+ return items.size
+ }
+ }
+
+ private class ButtonHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val button: MaterialButton = itemView.findViewById(R.id.item_button_function)
+
+ }
+
+ init {
+ AssertUtils.requireNonNull(mPhotos, "'mPhotos' not initialized")
+ }
+}
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/SimplePhotoPresenter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/SimplePhotoPresenter.kt
new file mode 100644
index 000000000..fbd8b8bab
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/SimplePhotoPresenter.kt
@@ -0,0 +1,69 @@
+package dev.ragnarok.fenrir.activity.photopager
+
+import android.content.Context
+import android.os.Bundle
+import dev.ragnarok.fenrir.fromIOToMain
+import dev.ragnarok.fenrir.model.AccessIdPair
+import dev.ragnarok.fenrir.model.Photo
+import dev.ragnarok.fenrir.util.Utils
+
+class SimplePhotoPresenter(
+ photos: ArrayList, index: Int, needToRefreshData: Boolean,
+ accountId: Int, context: Context, savedInstanceState: Bundle?
+) : PhotoPagerPresenter(photos, accountId, !needToRefreshData, context, savedInstanceState) {
+ private var mDataRefreshSuccessfull = false
+ private fun refreshData() {
+ val ids = ArrayList(mPhotos.size)
+ for (photo in mPhotos) {
+ ids.add(AccessIdPair(photo.getObjectId(), photo.ownerId, photo.accessKey))
+ }
+ appendDisposable(photosInteractor.getPhotosByIds(accountId, ids)
+ .fromIOToMain()
+ .subscribe({ onPhotosReceived(it) }) { t ->
+ view?.let {
+ showError(
+ it,
+ Utils.getCauseIfRuntime(t)
+ )
+ }
+ })
+ }
+
+ private fun onPhotosReceived(photos: List) {
+ mDataRefreshSuccessfull = true
+ onPhotoListRefresh(photos)
+ }
+
+ private fun onPhotoListRefresh(photos: List) {
+ val originalData: MutableList = mPhotos
+ for (photo in photos) {
+ //замена старых обьектов новыми
+ for (i in originalData.indices) {
+ val orig = originalData[i]
+ if (orig.getObjectId() == photo.getObjectId() && orig.ownerId == photo.ownerId) {
+ originalData[i] = photo
+
+ // если у фото до этого не было ссылок на файлы
+ if (orig.sizes == null || orig.sizes?.isEmpty() == true) {
+ view?.rebindPhotoAt(
+ i
+ )
+ }
+ break
+ }
+ }
+ }
+ refreshInfoViews(true)
+ }
+
+ override fun close() {
+ view?.returnOnlyPos(currentIndex)
+ }
+
+ init {
+ currentIndex = index
+ if (needToRefreshData && !mDataRefreshSuccessfull) {
+ refreshData()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/TmpGalleryPagerPresenter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/TmpGalleryPagerPresenter.kt
new file mode 100644
index 000000000..aa6e5de66
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/photopager/TmpGalleryPagerPresenter.kt
@@ -0,0 +1,56 @@
+package dev.ragnarok.fenrir.activity.photopager
+
+import android.content.Context
+import android.os.Bundle
+import dev.ragnarok.fenrir.db.Stores
+import dev.ragnarok.fenrir.db.serialize.Serializers
+import dev.ragnarok.fenrir.fromIOToMain
+import dev.ragnarok.fenrir.model.Photo
+import dev.ragnarok.fenrir.model.TmpSource
+import dev.ragnarok.fenrir.module.parcel.ParcelNative
+import dev.ragnarok.fenrir.util.PersistentLogger
+
+class TmpGalleryPagerPresenter : PhotoPagerPresenter {
+ constructor(
+ accountId: Int, source: TmpSource, index: Int, context: Context,
+ savedInstanceState: Bundle?
+ ) : super(ArrayList(0), accountId, false, context, savedInstanceState) {
+ currentIndex = index
+ loadDataFromDatabase(source)
+ }
+
+ constructor(
+ accountId: Int, source: Long, index: Int, context: Context,
+ savedInstanceState: Bundle?
+ ) : super(ArrayList(0), accountId, false, context, savedInstanceState) {
+ currentIndex = index
+ changeLoadingNowState(true)
+ onInitialLoadingFinished(
+ ParcelNative.fromNative(source).readParcelableList(Photo.NativeCreator)!!
+ )
+ }
+
+ override fun close() {
+ view?.returnOnlyPos(currentIndex)
+ }
+
+ private fun loadDataFromDatabase(source: TmpSource) {
+ changeLoadingNowState(true)
+ appendDisposable(Stores.instance
+ .tempStore()
+ .getTemporaryData(source.ownerId, source.sourceId, Serializers.PHOTOS_SERIALIZER)
+ .fromIOToMain()
+ .subscribe({ onInitialLoadingFinished(it) }) {
+ PersistentLogger.logThrowable("TmpGalleryPagerPresenter", it)
+ })
+ }
+
+ private fun onInitialLoadingFinished(photos: List) {
+ changeLoadingNowState(false)
+ mPhotos.addAll(photos)
+ refreshPagerView()
+ resolveButtonsBarVisible()
+ resolveToolbarVisibility()
+ refreshInfoViews(true)
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/qr/CameraScanActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/qr/CameraScanActivity.kt
new file mode 100644
index 000000000..c5c25c9f3
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/qr/CameraScanActivity.kt
@@ -0,0 +1,487 @@
+package dev.ragnarok.fenrir.activity.qr
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.graphics.*
+import android.os.Build
+import android.os.Bundle
+import android.util.AttributeSet
+import android.util.Log
+import android.util.Size
+import android.view.MotionEvent
+import android.view.View
+import androidx.camera.core.*
+import androidx.camera.core.Camera
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.PreviewView
+import androidx.core.content.ContextCompat
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.google.zxing.*
+import com.google.zxing.common.HybridBinarizer
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.NoMainActivity
+import dev.ragnarok.fenrir.activity.slidr.Slidr
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.nonNullNoEmpty
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.util.AppPerms
+import dev.ragnarok.fenrir.util.AppPerms.requestPermissionsResultAbs
+import dev.ragnarok.fenrir.util.Utils
+import java.nio.ByteBuffer
+import java.util.*
+
+class CameraScanActivity : NoMainActivity() {
+ private lateinit var textureView: PreviewView
+ private var camera: Camera? = null
+ private var finder: FinderView? = null
+ private var flashButton: FloatingActionButton? = null
+ private val reader: MultiFormatReader = MultiFormatReader()
+ private var isFlash = false
+
+ inner class DecoderResultPointCallback : ResultPointCallback {
+ override fun foundPossibleResultPoint(point: ResultPoint) {
+ finder?.pushPoints(point)
+ }
+ }
+
+ init {
+ val hints: MutableMap = EnumMap(DecodeHintType::class.java)
+ hints[DecodeHintType.NEED_RESULT_POINT_CALLBACK] = DecoderResultPointCallback()
+ hints[DecodeHintType.POSSIBLE_FORMATS] = EnumSet.of(
+ BarcodeFormat.QR_CODE,
+ BarcodeFormat.EAN_13,
+ BarcodeFormat.EAN_8,
+ BarcodeFormat.RSS_14,
+ BarcodeFormat.CODE_39,
+ BarcodeFormat.CODE_93,
+ BarcodeFormat.CODE_128,
+ BarcodeFormat.ITF
+ )
+ reader.setHints(hints)
+ }
+
+ private val requestCameraPermission = requestPermissionsResultAbs(
+ arrayOf(
+ Manifest.permission.CAMERA
+ ), {
+ startCamera()
+ }, { finish() })
+
+ private fun updateFlashButton() {
+ Utils.setColorFilter(
+ flashButton,
+ if (isFlash) CurrentTheme.getColorPrimary(this) else ContextCompat.getColor(
+ this,
+ com.google.android.material.R.color.m3_fab_efab_foreground_color_selector
+ )
+ )
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Slidr.attach(
+ this,
+ SlidrConfig.Builder().scrimColor(CurrentTheme.getColorBackground(this)).build()
+ )
+ setContentView(R.layout.activity_camera_scan)
+ textureView = findViewById(R.id.preview)
+ finder = findViewById(R.id.view_finder)
+ flashButton = findViewById(R.id.flash_button)
+ updateFlashButton()
+
+ flashButton?.setOnClickListener {
+ if (camera?.cameraInfo?.hasFlashUnit() == true) {
+ isFlash = !isFlash
+ camera?.cameraControl?.enableTorch(isFlash)
+ updateFlashButton()
+ }
+ }
+
+ if (AppPerms.hasCameraPermission(this)) {
+ startCamera()
+ } else {
+ requestCameraPermission.launch()
+ }
+ }
+
+ private fun detect(generatedQRCode: Bitmap): String? {
+ val width = generatedQRCode.width
+ val height = generatedQRCode.height
+ val pixels = IntArray(width * height)
+ generatedQRCode.getPixels(pixels, 0, width, 0, 0, width, height)
+ val source = RGBLuminanceSource(width, height, pixels)
+ val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
+
+ val result: Result = try {
+ reader.decodeWithState(binaryBitmap)
+ } catch (e: Exception) {
+ return e.localizedMessage
+ } ?: run {
+ return "error"
+ }
+ finder?.possibleResultPoints?.clear()
+ finder?.invalidate()
+
+ return result.text
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private fun startCamera() {
+ val preview = Preview.Builder().build()
+ preview.setSurfaceProvider(textureView.surfaceProvider)
+ val imageAnalysis = ImageAnalysis.Builder()
+ .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
+ .setTargetAspectRatio(AspectRatio.RATIO_4_3)
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+ .build()
+ imageAnalysis.setAnalyzer(
+ ContextCompat.getMainExecutor(this)
+ ) { imageProxy: ImageProxy ->
+ @SuppressLint("UnsafeOptInUsageError") val image =
+ imageProxy.image ?: return@setAnalyzer imageProxy.close()
+ val aspect = image.width.coerceAtMost(image.height)
+ finder?.updatePreviewSize(aspect, aspect)
+ val firstBuffer = image.planes[0].buffer
+ firstBuffer.rewind()
+ val firstBytes = ByteArray(firstBuffer.remaining())
+ firstBuffer[firstBytes]
+ val bmp = Bitmap.createBitmap(
+ image.width, image.height,
+ Bitmap.Config.ARGB_8888
+ )
+ val buffer = ByteBuffer.wrap(firstBytes)
+ bmp.copyPixelsFromBuffer(buffer)
+ val m = Matrix()
+ m.postRotate(imageProxy.imageInfo.rotationDegrees.toFloat())
+ val res = Bitmap.createBitmap(
+ bmp,
+ (image.width - aspect) / 2,
+ (image.height - aspect) / 2,
+ aspect,
+ aspect,
+ m,
+ true
+ )
+ if (res != bmp) {
+ bmp.recycle()
+ }
+ val data = detect(res)
+ if (data.nonNullNoEmpty()) {
+ val retIntent = Intent()
+ retIntent.putExtra(Extra.URL, data)
+ setResult(Activity.RESULT_OK, retIntent)
+ imageProxy.close()
+ finish()
+ return@setAnalyzer
+ }
+ imageProxy.close()
+ }
+
+ // request a ProcessCameraProvider
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
+
+ // verify that initialization succeeded when View was created
+ cameraProviderFuture.addListener({
+ try {
+ val cameraProvider = cameraProviderFuture.get()
+ camera = cameraProvider.bindToLifecycle(
+ this, CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+ .build(),
+ imageAnalysis, preview
+ )
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }, ContextCompat.getMainExecutor(this))
+
+ textureView.setOnTouchListener { _, event ->
+ return@setOnTouchListener when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ true
+ }
+ MotionEvent.ACTION_UP -> {
+ val factory: MeteringPointFactory = SurfaceOrientedMeteringPointFactory(
+ textureView.width.toFloat(), textureView.height.toFloat()
+ )
+ val autoFocusPoint = factory.createPoint(event.x, event.y)
+ try {
+ camera?.cameraControl?.startFocusAndMetering(
+ FocusMeteringAction.Builder(
+ autoFocusPoint,
+ FocusMeteringAction.FLAG_AF
+ ).apply {
+ //focus only when the user tap the preview
+ disableAutoCancel()
+ }.build()
+ )
+ } catch (e: CameraInfoUnavailableException) {
+ Log.d("ERROR", "cannot access camera", e)
+ }
+ true
+ }
+ else -> false // Unhandled event.
+ }
+ }
+ }
+
+ open class FinderView(context: Context, attrs: AttributeSet?) :
+ View(context, attrs) {
+ private val frame: Rect = Rect()
+ private val rectTmp: RectF = RectF()
+ private val path = Path()
+ private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val cornerPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val laserPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val pointPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val POINT_SIZE = 6f
+ private var scannerAlpha = 0
+ private val SCANNER_ALPHA = intArrayOf(0, 64, 128, 192, 255, 192, 128, 64)
+ private val ANIMATION_DELAY = 80L
+ private var previewSize = Size(0, 0)
+ var possibleResultPoints: ArrayList = ArrayList()
+
+ fun updatePreviewSize(width: Int, height: Int) {
+ if (previewSize.height != height || previewSize.width != width) {
+ previewSize = Size(width, height)
+ invalidate()
+ }
+ }
+
+ fun pushPoints(p: ResultPoint) {
+ possibleResultPoints.add(p)
+ if (possibleResultPoints.size > 20) {
+ possibleResultPoints.clear()
+ }
+ }
+
+ init {
+ cornerPaint.color = Color.parseColor("#ffffff")
+ paint.color = Color.parseColor("#88000000")
+ laserPaint.color = CurrentTheme.getColorInActive(context)
+ pointPaint.color = CurrentTheme.getColorPrimary(context)
+ }
+
+ private fun aroundPoint(x: Int, y: Int, r: Int): RectF {
+ rectTmp.set((x - r).toFloat(), (y - r).toFloat(), (x + r).toFloat(), (y + r).toFloat())
+ return rectTmp
+ }
+
+ private fun lerp(a: Int, b: Int, f: Float): Int {
+ return (a + f * (b - a)).toInt()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ val s = width.coerceAtMost(height) - Utils.dp(10f)
+ frame.left = (width - s) / 2
+ frame.top = (height - s) / 2
+ frame.bottom = frame.top + s
+ frame.right = frame.left + s
+ val width: Int = width
+ val height: Int = height
+
+ canvas.drawRect(0f, 0f, width.toFloat(), frame.top.toFloat(), paint)
+ canvas.drawRect(
+ 0f,
+ frame.top.toFloat(),
+ frame.left.toFloat(),
+ (frame.bottom + 1).toFloat(),
+ paint
+ )
+ canvas.drawRect(
+ (frame.right + 1).toFloat(),
+ frame.top.toFloat(),
+ width.toFloat(),
+ (frame.bottom + 1).toFloat(),
+ paint
+ )
+ canvas.drawRect(
+ 0f,
+ (frame.bottom + 1).toFloat(),
+ width.toFloat(),
+ height.toFloat(),
+ paint
+ )
+ val lineWidth =
+ lerp(0, Utils.dp(4f), 1f)
+ val halfLineWidth = lineWidth / 2
+ val lineLength = lerp(
+ (frame.right - frame.left).coerceAtMost(frame.bottom - frame.top),
+ Utils.dp(20f),
+ 1f
+ )
+ path.reset()
+ path.arcTo(aroundPoint(frame.left, frame.top + lineLength, halfLineWidth), 0f, 180f)
+ path.arcTo(
+ aroundPoint(
+ (frame.left + lineWidth * 1.5f).toInt(),
+ (frame.top + lineWidth * 1.5f).toInt(), lineWidth * 2
+ ), 180f, 90f
+ )
+ path.arcTo(aroundPoint(frame.left + lineLength, frame.top, halfLineWidth), 270f, 180f)
+ path.lineTo(
+ (frame.left + halfLineWidth).toFloat(),
+ (frame.top + halfLineWidth).toFloat()
+ )
+ path.arcTo(
+ aroundPoint(
+ (frame.left + lineWidth * 1.5f).toInt(),
+ (frame.top + lineWidth * 1.5f).toInt(), lineWidth
+ ), 270f, -90f
+ )
+ path.close()
+ canvas.drawPath(path, cornerPaint)
+ path.reset()
+ path.arcTo(aroundPoint(frame.right, frame.top + lineLength, halfLineWidth), 180f, -180f)
+ path.arcTo(
+ aroundPoint(
+ (frame.right - lineWidth * 1.5f).toInt(),
+ (frame.top + lineWidth * 1.5f).toInt(), lineWidth * 2
+ ), 0f, -90f
+ )
+ path.arcTo(aroundPoint(frame.right - lineLength, frame.top, halfLineWidth), 270f, -180f)
+ path.arcTo(
+ aroundPoint(
+ (frame.right - lineWidth * 1.5f).toInt(),
+ (frame.top + lineWidth * 1.5f).toInt(), lineWidth
+ ), 270f, 90f
+ )
+ path.close()
+ canvas.drawPath(path, cornerPaint)
+ path.reset()
+ path.arcTo(aroundPoint(frame.left, frame.bottom - lineLength, halfLineWidth), 0f, -180f)
+ path.arcTo(
+ aroundPoint(
+ (frame.left + lineWidth * 1.5f).toInt(),
+ (frame.bottom - lineWidth * 1.5f).toInt(), lineWidth * 2
+ ), 180f, -90f
+ )
+ path.arcTo(
+ aroundPoint(frame.left + lineLength, frame.bottom, halfLineWidth),
+ 90f,
+ -180f
+ )
+ path.arcTo(
+ aroundPoint(
+ (frame.left + lineWidth * 1.5f).toInt(),
+ (frame.bottom - lineWidth * 1.5f).toInt(), lineWidth
+ ), 90f, 90f
+ )
+ path.close()
+ canvas.drawPath(path, cornerPaint)
+ path.reset()
+ path.arcTo(
+ aroundPoint(frame.right, frame.bottom - lineLength, halfLineWidth),
+ 180f,
+ 180f
+ )
+ path.arcTo(
+ aroundPoint(
+ (frame.right - lineWidth * 1.5f).toInt(),
+ (frame.bottom - lineWidth * 1.5f).toInt(), lineWidth * 2
+ ), 0f, 90f
+ )
+ path.arcTo(
+ aroundPoint(frame.right - lineLength, frame.bottom, halfLineWidth),
+ 90f,
+ 180f
+ )
+ path.arcTo(
+ aroundPoint(
+ (frame.right - lineWidth * 1.5f).toInt(),
+ (frame.bottom - lineWidth * 1.5f).toInt(), lineWidth
+ ), 90f, -90f
+ )
+ path.close()
+ canvas.drawPath(path, cornerPaint)
+
+ laserPaint.alpha = SCANNER_ALPHA[scannerAlpha]
+ if (scannerAlpha == 0) {
+ possibleResultPoints.clear()
+ }
+ scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.size
+
+ val middle = frame.height() / 2 + frame.top
+ canvas.drawRect(
+ (frame.left + 2).toFloat(),
+ (middle - 1).toFloat(),
+ (frame.right - 1).toFloat(),
+ (middle + 2).toFloat(),
+ laserPaint
+ )
+
+ if (previewSize.width > 0 && previewSize.height > 0) {
+ val scaleX: Float = frame.width() / previewSize.width.toFloat()
+ val scaleY: Float = frame.height() / previewSize.height.toFloat()
+
+ // draw current possible result points
+ if (possibleResultPoints.isNotEmpty()) {
+ for (point in possibleResultPoints) {
+ canvas.drawCircle(
+ frame.left + point.x * scaleX,
+ frame.top + point.y * scaleY,
+ POINT_SIZE, pointPaint
+ )
+ }
+ }
+ }
+
+ postInvalidateDelayed(
+ ANIMATION_DELAY,
+ (frame.left - POINT_SIZE).toInt(),
+ (frame.top - POINT_SIZE).toInt(),
+ (frame.right + POINT_SIZE).toInt(),
+ (frame.bottom + POINT_SIZE).toInt()
+ )
+ }
+ }
+
+ companion object {
+ fun decodeFromBitmap(gen: Bitmap?): String? {
+ if (gen == null) {
+ return "error"
+ }
+ var generatedQRCode: Bitmap = gen
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && generatedQRCode.config == Bitmap.Config.HARDWARE) {
+ generatedQRCode = generatedQRCode.copy(Bitmap.Config.ARGB_8888, true)
+ if (generatedQRCode == null) {
+ return "error"
+ }
+ }
+ val reader = MultiFormatReader()
+ val hints: MutableMap = EnumMap(DecodeHintType::class.java)
+ hints[DecodeHintType.POSSIBLE_FORMATS] = EnumSet.of(
+ BarcodeFormat.QR_CODE,
+ BarcodeFormat.EAN_13,
+ BarcodeFormat.EAN_8,
+ BarcodeFormat.RSS_14,
+ BarcodeFormat.CODE_39,
+ BarcodeFormat.CODE_93,
+ BarcodeFormat.CODE_128,
+ BarcodeFormat.ITF
+ )
+ reader.setHints(hints)
+
+ val width = generatedQRCode.width
+ val height = generatedQRCode.height
+ val pixels = IntArray(width * height)
+ generatedQRCode.getPixels(pixels, 0, width, 0, 0, width, height)
+ val source = RGBLuminanceSource(width, height, pixels)
+ val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
+
+ val result: Result = try {
+ reader.decodeWithState(binaryBitmap)
+ } catch (e: Exception) {
+ return e.localizedMessage
+ } ?: run {
+ return "error"
+ }
+
+ return result.text
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/qr/CustomQRCodeWriter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/qr/CustomQRCodeWriter.kt
new file mode 100644
index 000000000..371b52044
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/qr/CustomQRCodeWriter.kt
@@ -0,0 +1,244 @@
+package dev.ragnarok.fenrir.activity.qr
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import com.google.zxing.EncodeHintType
+import com.google.zxing.WriterException
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
+import com.google.zxing.qrcode.encoder.ByteMatrix
+import com.google.zxing.qrcode.encoder.Encoder
+import java.util.*
+import kotlin.math.roundToInt
+
+class CustomQRCodeWriter {
+ private val radii = FloatArray(8)
+ private lateinit var input: ByteMatrix
+ private var imageBloks = 0
+ private var imageBlockX = 0
+ private var sideQuadSize = 0
+ var imageSize = 0
+ private set
+
+ fun encode(contents: String, width: Int, height: Int, icon: Drawable?): Bitmap? {
+ try {
+ val hints = HashMap()
+ hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.Q
+ hints[EncodeHintType.MARGIN] = 0
+ return encode(contents, width, height, hints, icon)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ return null
+ }
+
+ @Throws(WriterException::class)
+ fun encode(
+ contents: String,
+ width: Int,
+ height: Int,
+ hints: Map?,
+ icon: Drawable?
+ ): Bitmap? {
+ require(contents.isNotEmpty()) { "Found empty contents" }
+ require(!(width < 0 || height < 0)) { "Requested dimensions are too small: " + width + 'x' + height }
+ var errorCorrectionLevel = ErrorCorrectionLevel.L
+ var quietZone = QUIET_ZONE_SIZE
+ if (hints != null) {
+ if (hints.containsKey(EncodeHintType.ERROR_CORRECTION)) {
+ errorCorrectionLevel =
+ ErrorCorrectionLevel.valueOf(hints[EncodeHintType.ERROR_CORRECTION].toString())
+ }
+ if (hints.containsKey(EncodeHintType.MARGIN)) {
+ quietZone = hints[EncodeHintType.MARGIN].toString().toInt()
+ }
+ }
+ val code = Encoder.encode(contents, errorCorrectionLevel, hints)
+ checkNotNull(code.matrix)
+ input = code.matrix
+ val inputWidth = input.width
+ val inputHeight = input.height
+ for (x in 0 until inputWidth) {
+ if (has(x, 0)) {
+ sideQuadSize++
+ } else {
+ break
+ }
+ }
+ val qrWidth = inputWidth + quietZone * 2
+ val qrHeight = inputHeight + quietZone * 2
+ val outputWidth = width.coerceAtLeast(qrWidth)
+ val outputHeight = height.coerceAtLeast(qrHeight)
+ val multiple = (outputWidth / qrWidth).coerceAtMost(outputHeight / qrHeight)
+ val padding = 16
+ val size = multiple * inputWidth + padding * 2
+ val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(bitmap)
+ canvas.drawColor(-0x1)
+ val blackPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ blackPaint.color = -0x1000000
+ val rect = GradientDrawable()
+ rect.shape = GradientDrawable.RECTANGLE
+ rect.cornerRadii = radii
+ imageBloks = ((size - 32) / 4.65f / multiple).roundToInt()
+ if (imageBloks % 2 != inputWidth % 2) {
+ imageBloks++
+ }
+ imageBlockX = (inputWidth - imageBloks) / 2
+ imageSize = imageBloks * multiple - 24
+ val imageX = (size - imageSize) / 2
+ for (a in 0..2) {
+ var x: Int
+ var y: Int
+ when (a) {
+ 0 -> {
+ x = padding
+ y = padding
+ }
+ 1 -> {
+ x = size - sideQuadSize * multiple - padding
+ y = padding
+ }
+ else -> {
+ x = padding
+ y = size - sideQuadSize * multiple - padding
+ }
+ }
+ var r = sideQuadSize * multiple / 3.0f
+ Arrays.fill(radii, r)
+ rect.setColor(-0x1000000)
+ rect.setBounds(x, y, x + sideQuadSize * multiple, y + sideQuadSize * multiple)
+ rect.draw(canvas)
+ canvas.drawRect(
+ (x + multiple).toFloat(),
+ (y + multiple).toFloat(),
+ (x + (sideQuadSize - 1) * multiple).toFloat(),
+ (y + (sideQuadSize - 1) * multiple).toFloat(),
+ blackPaint
+ )
+ r = sideQuadSize * multiple / 4.0f
+ Arrays.fill(radii, r)
+ rect.setColor(-0x1)
+ rect.setBounds(
+ x + multiple,
+ y + multiple,
+ x + (sideQuadSize - 1) * multiple,
+ y + (sideQuadSize - 1) * multiple
+ )
+ rect.draw(canvas)
+ r = (sideQuadSize - 2) * multiple / 4.0f
+ Arrays.fill(radii, r)
+ rect.setColor(-0x1000000)
+ rect.setBounds(
+ x + multiple * 2,
+ y + multiple * 2,
+ x + (sideQuadSize - 2) * multiple,
+ y + (sideQuadSize - 2) * multiple
+ )
+ rect.draw(canvas)
+ }
+ val r = multiple / 2.0f
+ var y = 0
+ var outputY = padding
+ while (y < inputHeight) {
+ var x = 0
+ var outputX = padding
+ while (x < inputWidth) {
+ if (has(x, y)) {
+ Arrays.fill(radii, r)
+ if (has(x, y - 1)) {
+ radii[1] = 0f
+ radii[0] = radii[1]
+ radii[3] = 0f
+ radii[2] = radii[3]
+ }
+ if (has(x, y + 1)) {
+ radii[7] = 0f
+ radii[6] = radii[7]
+ radii[5] = 0f
+ radii[4] = radii[5]
+ }
+ if (has(x - 1, y)) {
+ radii[1] = 0f
+ radii[0] = radii[1]
+ radii[7] = 0f
+ radii[6] = radii[7]
+ }
+ if (has(x + 1, y)) {
+ radii[3] = 0f
+ radii[2] = radii[3]
+ radii[5] = 0f
+ radii[4] = radii[5]
+ }
+ rect.setColor(-0x1000000)
+ rect.setBounds(outputX, outputY, outputX + multiple, outputY + multiple)
+ rect.draw(canvas)
+ } else {
+ var has = false
+ Arrays.fill(radii, 0f)
+ if (has(x - 1, y - 1) && has(x - 1, y) && has(x, y - 1)) {
+ radii[1] = r
+ radii[0] = radii[1]
+ has = true
+ }
+ if (has(x + 1, y - 1) && has(x + 1, y) && has(x, y - 1)) {
+ radii[3] = r
+ radii[2] = radii[3]
+ has = true
+ }
+ if (has(x - 1, y + 1) && has(x - 1, y) && has(x, y + 1)) {
+ radii[7] = r
+ radii[6] = radii[7]
+ has = true
+ }
+ if (has(x + 1, y + 1) && has(x + 1, y) && has(x, y + 1)) {
+ radii[5] = r
+ radii[4] = radii[5]
+ has = true
+ }
+ if (has) {
+ canvas.drawRect(
+ outputX.toFloat(),
+ outputY.toFloat(),
+ (outputX + multiple).toFloat(),
+ (outputY + multiple).toFloat(),
+ blackPaint
+ )
+ rect.setColor(-0x1)
+ rect.setBounds(outputX, outputY, outputX + multiple, outputY + multiple)
+ rect.draw(canvas)
+ }
+ }
+ x++
+ outputX += multiple
+ }
+ y++
+ outputY += multiple
+ }
+ if (icon != null) {
+ val drawable = icon.mutate()
+ drawable.setBounds(imageX, imageX, imageX + imageSize, imageX + imageSize)
+ drawable.draw(canvas)
+ }
+ canvas.setBitmap(null)
+ return bitmap
+ }
+
+ private fun has(x: Int, y: Int): Boolean {
+ if (x >= imageBlockX && x < imageBlockX + imageBloks && y >= imageBlockX && y < imageBlockX + imageBloks) {
+ return false
+ }
+ if ((x < sideQuadSize || x >= input.width - sideQuadSize) && y < sideQuadSize) {
+ return false
+ }
+ return if (x < sideQuadSize && y >= input.height - sideQuadSize) {
+ false
+ } else x >= 0 && y >= 0 && x < input.width && y < input.height && input[x, y].toInt() == 1
+ }
+
+ companion object {
+ private const val QUIET_ZONE_SIZE = 4
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/selectprofiles/SelectProfilesActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/selectprofiles/SelectProfilesActivity.kt
new file mode 100644
index 000000000..04ee10892
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/selectprofiles/SelectProfilesActivity.kt
@@ -0,0 +1,144 @@
+package dev.ragnarok.fenrir.activity.selectprofiles
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import dev.ragnarok.fenrir.Extra
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.MainActivity
+import dev.ragnarok.fenrir.activity.ProfileSelectable
+import dev.ragnarok.fenrir.fragment.fave.FaveTabsFragment
+import dev.ragnarok.fenrir.fragment.friends.friendstabs.FriendsTabsFragment
+import dev.ragnarok.fenrir.getParcelableArrayListCompat
+import dev.ragnarok.fenrir.getParcelableExtraCompat
+import dev.ragnarok.fenrir.model.Owner
+import dev.ragnarok.fenrir.model.SelectProfileCriteria
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceFactory
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.util.Logger
+import dev.ragnarok.fenrir.util.MainActivityTransforms
+import dev.ragnarok.fenrir.util.Utils
+
+class SelectProfilesActivity : MainActivity(), SelectedProfilesAdapter.ActionListener,
+ ProfileSelectable {
+ override var acceptableCriteria: SelectProfileCriteria? = null
+ private set
+ private var mSelectedOwners: ArrayList? = null
+ private var mRecyclerView: RecyclerView? = null
+ private var mProfilesAdapter: SelectedProfilesAdapter? = null
+
+ @MainActivityTransforms
+ override fun getMainActivityTransform(): Int {
+ return MainActivityTransforms.PROFILES
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ mLayoutRes = if (Settings.get()
+ .other().is_side_navigation()
+ ) R.layout.activity_main_with_profiles_selection_side else R.layout.activity_main_with_profiles_selection
+ super.onCreate(savedInstanceState)
+ mLastBackPressedTime = Long.MAX_VALUE - DOUBLE_BACK_PRESSED_TIMEOUT
+ acceptableCriteria = intent.getParcelableExtraCompat(Extra.CRITERIA)
+ if (savedInstanceState != null) {
+ mSelectedOwners = savedInstanceState.getParcelableArrayListCompat(SAVE_SELECTED_OWNERS)
+ }
+ if (mSelectedOwners == null) {
+ mSelectedOwners = ArrayList()
+ }
+ val manager: RecyclerView.LayoutManager =
+ LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
+ mProfilesAdapter = SelectedProfilesAdapter(this, mSelectedOwners ?: return)
+ mProfilesAdapter?.setActionListener(this)
+ mRecyclerView = findViewById(R.id.recycleView)
+ checkNotNull(mRecyclerView) { "Invalid view" }
+ mRecyclerView?.layoutManager = manager
+ mRecyclerView?.adapter = mProfilesAdapter
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putParcelableArrayList(SAVE_SELECTED_OWNERS, mSelectedOwners)
+ }
+
+ override fun onClick(adapterPosition: Int, owner: Owner) {
+ mProfilesAdapter?.toDataPosition(adapterPosition)?.let { mSelectedOwners?.removeAt(it) }
+ mProfilesAdapter?.notifyItemRemoved(adapterPosition)
+ mProfilesAdapter?.notifyHeaderChange()
+ }
+
+ override fun onCheckClick() {
+ val intent = Intent()
+ intent.putParcelableArrayListExtra(Extra.OWNERS, mSelectedOwners)
+ setResult(RESULT_OK, intent)
+ finish()
+ }
+
+ override fun select(owner: Owner) {
+ Logger.d(TAG, "Select, owner: $owner")
+ val index = Utils.indexOfOwner(mSelectedOwners, owner)
+ if (index != -1) {
+ mSelectedOwners?.removeAt(index)
+ mProfilesAdapter?.toAdapterPosition(index)
+ ?.let { mProfilesAdapter?.notifyItemRemoved(it) }
+ }
+ mSelectedOwners?.add(0, owner)
+ mProfilesAdapter?.toAdapterPosition(0)?.let { mProfilesAdapter?.notifyItemInserted(it) }
+ mProfilesAdapter?.notifyHeaderChange()
+ mRecyclerView?.smoothScrollToPosition(0)
+ }
+
+ companion object {
+ private val TAG = SelectProfilesActivity::class.java.simpleName
+ private const val SAVE_SELECTED_OWNERS = "save_selected_owners"
+
+
+ fun createIntent(
+ context: Context,
+ initialPlace: Place,
+ criteria: SelectProfileCriteria
+ ): Intent {
+ return Intent(context, SelectProfilesActivity::class.java)
+ .setAction(ACTION_OPEN_PLACE)
+ .putExtra(Extra.PLACE, initialPlace)
+ .putExtra(Extra.CRITERIA, criteria)
+ }
+
+
+ fun startFriendsSelection(context: Context): Intent {
+ val aid = Settings.get()
+ .accounts()
+ .current
+ val place = PlaceFactory.getFriendsFollowersPlace(
+ aid,
+ aid,
+ FriendsTabsFragment.TAB_ALL_FRIENDS,
+ null
+ )
+ val criteria =
+ SelectProfileCriteria().setOwnerType(SelectProfileCriteria.OwnerType.ONLY_FRIENDS)
+ val intent = Intent(context, SelectProfilesActivity::class.java)
+ intent.action = ACTION_OPEN_PLACE
+ intent.putExtra(Extra.PLACE, place)
+ intent.putExtra(Extra.CRITERIA, criteria)
+ return intent
+ }
+
+
+ fun startFaveSelection(context: Context): Intent {
+ val aid = Settings.get()
+ .accounts()
+ .current
+ val place = PlaceFactory.getBookmarksPlace(aid, FaveTabsFragment.TAB_PAGES)
+ val criteria =
+ SelectProfileCriteria().setOwnerType(SelectProfileCriteria.OwnerType.OWNERS)
+ val intent = Intent(context, SelectProfilesActivity::class.java)
+ intent.action = ACTION_OPEN_PLACE
+ intent.putExtra(Extra.PLACE, place)
+ intent.putExtra(Extra.CRITERIA, criteria)
+ return intent
+ }
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/selectprofiles/SelectedProfilesAdapter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/selectprofiles/SelectedProfilesAdapter.kt
new file mode 100644
index 000000000..a5304679b
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/selectprofiles/SelectedProfilesAdapter.kt
@@ -0,0 +1,131 @@
+package dev.ragnarok.fenrir.activity.selectprofiles
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.squareup.picasso3.Transformation
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.model.Community
+import dev.ragnarok.fenrir.model.Owner
+import dev.ragnarok.fenrir.model.User
+import dev.ragnarok.fenrir.picasso.PicassoInstance.Companion.with
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import java.util.*
+
+class SelectedProfilesAdapter(private val mContext: Context, private val mData: List) :
+ RecyclerView.Adapter() {
+ private val mTransformation: Transformation = CurrentTheme.createTransformationForAvatar()
+ private var mActionListener: ActionListener? = null
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ when (viewType) {
+ VIEW_TYPE_CHECK -> return CheckViewHolder(
+ LayoutInflater.from(mContext)
+ .inflate(R.layout.item_selection_check, parent, false)
+ )
+ VIEW_TYPE_USER -> return ProfileViewHolder(
+ LayoutInflater.from(mContext)
+ .inflate(R.layout.item_selected_user, parent, false)
+ )
+ }
+ throw UnsupportedOperationException()
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ if (position == 0) {
+ bindCheckViewHolder(holder as CheckViewHolder)
+ } else {
+ bindProfileViewHolder(holder as ProfileViewHolder, position)
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return if (position == 0) VIEW_TYPE_CHECK else VIEW_TYPE_USER
+ }
+
+ private fun bindCheckViewHolder(holder: CheckViewHolder) {
+ if (mData.isEmpty()) {
+ holder.counter.setText(R.string.press_plus_for_add)
+ } else {
+ holder.counter.text = mData.size.toString()
+ }
+ holder.root.setOnClickListener {
+ mActionListener?.onCheckClick()
+ }
+ }
+
+ private fun bindProfileViewHolder(holder: ProfileViewHolder, adapterPosition: Int) {
+ val owner = mData[toDataPosition(adapterPosition)]
+ var title: String? = null
+ var ava: String? = null
+ if (owner is User) {
+ title = owner.firstName
+ ava = owner.photo50
+ } else if (owner is Community) {
+ title = owner.fullName
+ ava = owner.photo50
+ }
+ holder.name.text = title
+ with()
+ .load(ava)
+ .transform(mTransformation)
+ .into(holder.avatar)
+ holder.buttonRemove.setOnClickListener {
+ mActionListener?.onClick(holder.bindingAdapterPosition, owner)
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return mData.size + 1
+ }
+
+ fun setActionListener(actionListener: ActionListener?) {
+ mActionListener = actionListener
+ }
+
+ fun toAdapterPosition(dataPosition: Int): Int {
+ return dataPosition + 1
+ }
+
+ fun toDataPosition(adapterPosition: Int): Int {
+ return adapterPosition - 1
+ }
+
+ fun notifyHeaderChange() {
+ notifyItemChanged(0)
+ }
+
+ interface ActionListener : EventListener {
+ fun onClick(adapterPosition: Int, owner: Owner)
+ fun onCheckClick()
+ }
+
+ private class CheckViewHolder(itemView: View) :
+ RecyclerView.ViewHolder(itemView) {
+ val counter: TextView = itemView.findViewById(R.id.counter)
+ val root: View = itemView.findViewById(R.id.root)
+ }
+
+ private inner class ProfileViewHolder(itemView: View) :
+ RecyclerView.ViewHolder(itemView) {
+ val avatar: ImageView = itemView.findViewById(R.id.avatar)
+ val name: TextView = itemView.findViewById(R.id.name)
+ val buttonRemove: ImageView = itemView.findViewById(R.id.button_remove)
+
+ init {
+ buttonRemove.drawable.setTint(CurrentTheme.getColorOnSurface(mContext))
+ val root = itemView.findViewById(R.id.root)
+ root.background.setTint(CurrentTheme.getMessageBackgroundSquare(mContext))
+ //root.getBackground().setAlpha(180);
+ }
+ }
+
+ companion object {
+ private const val VIEW_TYPE_CHECK = 0
+ private const val VIEW_TYPE_USER = 1
+ }
+
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/ColorPanelSlideListener.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/ColorPanelSlideListener.kt
new file mode 100644
index 000000000..4c4d6005a
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/ColorPanelSlideListener.kt
@@ -0,0 +1,93 @@
+package dev.ragnarok.fenrir.activity.slidr
+
+import android.animation.ArgbEvaluator
+import android.app.Activity
+import android.graphics.Color
+import android.view.View
+import androidx.annotation.ColorInt
+import androidx.core.graphics.ColorUtils
+import dev.ragnarok.fenrir.activity.slidr.widget.SliderPanel.OnPanelSlideListener
+import dev.ragnarok.fenrir.settings.CurrentTheme.getNavigationBarColor
+import dev.ragnarok.fenrir.settings.CurrentTheme.getStatusBarColor
+import dev.ragnarok.fenrir.settings.CurrentTheme.getStatusBarNonColored
+import dev.ragnarok.fenrir.util.Utils
+
+internal open class ColorPanelSlideListener(
+ private val activity: Activity,
+ private val isFromUnColoredToColoredStatusBar: Boolean,
+ private val isUseAlpha: Boolean
+) : OnPanelSlideListener {
+ private val evaluator = ArgbEvaluator()
+
+ @ColorInt
+ private val statusBarNonColored: Int = getStatusBarNonColored(activity)
+
+ @ColorInt
+ private val statusBarColored: Int = getStatusBarColor(activity)
+
+ @ColorInt
+ private val navigationBarNonColored: Int = Color.BLACK
+
+ @ColorInt
+ private val navigationBarColored: Int = getNavigationBarColor(activity)
+ override fun onStateChanged(state: Int) {
+ // Unused.
+ }
+
+ override fun onClosed() {
+ activity.finish()
+ activity.overridePendingTransition(0, 0)
+ }
+
+ override fun onOpened() {
+ // Unused.
+ }
+
+ private fun isDark(@ColorInt color: Int): Boolean {
+ return ColorUtils.calculateLuminance(color) < 0.5
+ }
+
+ @Suppress("DEPRECATION")
+ override fun onSlideChange(percent: Float) {
+ try {
+ if (isFromUnColoredToColoredStatusBar) {
+ val statusColor =
+ evaluator.evaluate(percent, statusBarColored, statusBarNonColored) as Int
+ val navigationColor = evaluator.evaluate(
+ percent,
+ navigationBarColored,
+ navigationBarNonColored
+ ) as Int
+ val w = activity.window
+ if (w != null) {
+ w.statusBarColor = statusColor
+ w.navigationBarColor = navigationColor
+ val invertIcons = !isDark(statusColor)
+ if (Utils.hasMarshmallow()) {
+ var flags = w.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ }
+ w.decorView.systemUiVisibility = flags
+ }
+ if (Utils.hasOreo()) {
+ var flags = w.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ }
+ w.decorView.systemUiVisibility = flags
+ }
+ }
+ }
+ if (isUseAlpha) {
+ activity.window.decorView.rootView.alpha = Utils.clamp(percent, 0f, 1f)
+ }
+ } catch (ignored: Exception) {
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/ConfigPanelSlideListener.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/ConfigPanelSlideListener.kt
new file mode 100644
index 000000000..a56e4eda3
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/ConfigPanelSlideListener.kt
@@ -0,0 +1,32 @@
+package dev.ragnarok.fenrir.activity.slidr
+
+import android.app.Activity
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+
+internal class ConfigPanelSlideListener(activity: Activity, private val config: SlidrConfig) :
+ ColorPanelSlideListener(activity, false, true) {
+ override fun onStateChanged(state: Int) {
+ config.listener?.onSlideStateChanged(state)
+ }
+
+ override fun onClosed() {
+ if (config.listener?.onSlideClosed() == true) {
+ return
+ }
+ super.onClosed()
+ }
+
+ override fun onOpened() {
+ config.listener?.onSlideOpened()
+ }
+
+ override fun onSlideChange(percent: Float) {
+ super.onSlideChange(percent)
+ config.listener?.onSlideChange(percent)
+ }
+
+ val isFromUnColoredToColoredStatusBar: Boolean
+ get() = config.isFromUnColoredToColoredStatusBar()
+ val isUseAlpha: Boolean
+ get() = config.isAlphaForView()
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/FragmentPanelSlideListener.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/FragmentPanelSlideListener.kt
new file mode 100644
index 000000000..cd03dec45
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/FragmentPanelSlideListener.kt
@@ -0,0 +1,38 @@
+package dev.ragnarok.fenrir.activity.slidr
+
+import android.view.View
+import androidx.fragment.app.FragmentActivity
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.widget.SliderPanel.OnPanelSlideListener
+
+internal class FragmentPanelSlideListener(private val view: View, private val config: SlidrConfig) :
+ OnPanelSlideListener {
+ override fun onStateChanged(state: Int) {
+ config.listener?.onSlideStateChanged(state)
+ }
+
+ override fun onClosed() {
+ if (config.listener?.onSlideClosed() == true) {
+ return
+ }
+
+ // Ensure that we are attached to a FragmentActivity
+ if (view.context is FragmentActivity) {
+ val activity = view.context as FragmentActivity
+ if (activity.supportFragmentManager.backStackEntryCount == 0) {
+ activity.finish()
+ activity.overridePendingTransition(0, 0)
+ } else {
+ activity.supportFragmentManager.popBackStack()
+ }
+ }
+ }
+
+ override fun onOpened() {
+ config.listener?.onSlideOpened()
+ }
+
+ override fun onSlideChange(percent: Float) {
+ config.listener?.onSlideChange(percent)
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/Slidr.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/Slidr.kt
new file mode 100644
index 000000000..f4d88548e
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/Slidr.kt
@@ -0,0 +1,118 @@
+package dev.ragnarok.fenrir.activity.slidr
+
+import android.app.Activity
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrInterface
+import dev.ragnarok.fenrir.activity.slidr.widget.SliderPanel
+
+/**
+ * This attacher class is used to attach the sliding mechanism to any [android.app.Activity]
+ * that lets the user slide (or swipe) the activity away as a form of back or up action. The action
+ * causes [android.app.Activity.finish] to be called.
+ */
+object Slidr {
+ /**
+ * Attach a slideable mechanism to an activity that adds the slide to dismiss functionality
+ *
+ * @param activity the activity to attach the slider to
+ * @return a [dev.ragnarok.fenrir.activity.slidr.model.SlidrInterface] that allows
+ * the user to lock/unlock the sliding mechanism for whatever purpose.
+ */
+ fun attach(
+ activity: Activity,
+ fromUnColoredToColoredStatusBar: Boolean = false,
+ useAlpha: Boolean = true
+ ): SlidrInterface {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ activity.setTranslucent(true)
+ }
+ activity.window.setBackgroundDrawableResource(R.color.transparent)
+
+ // Setup the slider panel and attach it to the decor
+ val panel = attachSliderPanel(activity, null)
+
+ // Set the panel slide listener for when it becomes closed or opened
+ panel.setOnPanelSlideListener(
+ ColorPanelSlideListener(
+ activity,
+ fromUnColoredToColoredStatusBar,
+ useAlpha
+ )
+ )
+
+ // Return the lock interface
+ return panel.defaultInterface
+ }
+
+ /**
+ * Attach a slider mechanism to an activity based on the passed [dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig]
+ *
+ * @param activity the activity to attach the slider to
+ * @param config the slider configuration to make
+ * @return a [dev.ragnarok.fenrir.activity.slidr.model.SlidrInterface] that allows
+ * the user to lock/unlock the sliding mechanism for whatever purpose.
+ */
+ fun attach(activity: Activity, config: SlidrConfig): SlidrInterface {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ activity.setTranslucent(true)
+ }
+ activity.window.setBackgroundDrawableResource(R.color.transparent)
+ // Setup the slider panel and attach it to the decor
+ val panel = attachSliderPanel(activity, config)
+
+ // Set the panel slide listener for when it becomes closed or opened
+ panel.setOnPanelSlideListener(ConfigPanelSlideListener(activity, config))
+
+ // Return the lock interface
+ return panel.defaultInterface
+ }
+
+ /**
+ * Attach a new [SliderPanel] to the root of the activity's content
+ */
+ private fun attachSliderPanel(activity: Activity, config: SlidrConfig?): SliderPanel {
+ // Hijack the decorview
+ val decorView = activity.window.decorView as ViewGroup
+ val oldScreen = decorView.getChildAt(0)
+ decorView.removeViewAt(0)
+
+ // Setup the slider panel and attach it to the decor
+ val panel = SliderPanel(activity, oldScreen, config)
+ panel.id = R.id.slidable_panel
+ oldScreen.id = R.id.slidable_content
+ panel.addView(oldScreen)
+ decorView.addView(panel, 0)
+ return panel
+ }
+
+ /**
+ * Attach a slider mechanism to a fragment view replacing an internal view
+ *
+ * @param oldScreen the view within a fragment to replace
+ * @param config the slider configuration to attach with
+ * @return a [dev.ragnarok.fenrir.activity.slidr.model.SlidrInterface] that allows
+ * the user to lock/unlock the sliding mechanism for whatever purpose.
+ */
+ fun replace(oldScreen: View, config: SlidrConfig): SlidrInterface {
+ val parent = oldScreen.parent as ViewGroup
+ val params = oldScreen.layoutParams
+ parent.removeView(oldScreen)
+
+ // Setup the slider panel and attach it
+ val panel = SliderPanel(oldScreen.context, oldScreen, config)
+ panel.id = R.id.slidable_panel
+ oldScreen.id = R.id.slidable_content
+ panel.addView(oldScreen)
+ parent.addView(panel, 0, params)
+
+ // Set the panel slide listener for when it becomes closed or opened
+ panel.setOnPanelSlideListener(FragmentPanelSlideListener(oldScreen, config))
+
+ // Return the lock interface
+ return panel.defaultInterface
+ }
+}
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrConfig.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrConfig.kt
new file mode 100644
index 000000000..3ade5b423
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrConfig.kt
@@ -0,0 +1,237 @@
+package dev.ragnarok.fenrir.activity.slidr.model
+
+import androidx.annotation.ColorInt
+import androidx.annotation.FloatRange
+import dev.ragnarok.fenrir.settings.Settings.get
+
+/**
+ * This class contains the configuration information for all the options available in
+ * this library
+ */
+class SlidrConfig internal constructor() {
+ private var fromUnColoredToColoredStatusBar = false
+ private var sensitivity = 0.5f
+ private var scrimColor = -1
+ private var scrimStartAlpha = 0.8f
+ private var scrimEndAlpha = 0f
+ private var velocityThreshold = 5f
+ private var distanceThreshold = 0.25f
+
+ /**
+ * Has the user configured slidr to only catch at the edge of the screen ?
+ *
+ * @return true if is edge capture only
+ */
+ var isEdgeOnly = false
+ private set
+ private var edgeSize = 0.18f
+
+ /**
+ * Has the user configured slidr to ignore all scrollable children inside
+ *
+ * @return true if is ignore all scrollable children under touch
+ */
+ var isIgnoreChildScroll = false
+ private set
+ private var alphaForView = true
+
+ /**
+ * Get the position of the slidable mechanism for this configuration. This is the position on
+ * the screen that the user can swipe the activity away from
+ *
+ * @return the slider position
+ */
+ var position = SlidrPosition.LEFT
+ private set
+
+ /**
+ * Get the slidr listener set by the user to respond to certain events in the sliding
+ * mechanism.
+ *
+ * @return the slidr listener
+ */
+ var listener: SlidrListener? = null
+ private set
+
+ fun isFromUnColoredToColoredStatusBar(): Boolean {
+ return fromUnColoredToColoredStatusBar
+ }
+
+ /***********************************************************************************************
+ *
+ * Setters
+ *
+ */
+ fun setFromUnColoredToColoredStatusBar(en: Boolean) {
+ fromUnColoredToColoredStatusBar = en
+ }
+
+ /**
+ * Get the color of the background scrim
+ *
+ * @return the scrim color integer
+ */
+ @ColorInt
+ fun getScrimColor(): Int {
+ return scrimColor
+ }
+
+ fun setScrimColor(@ColorInt scrimColor: Int) {
+ this.scrimColor = scrimColor
+ }
+
+ /**
+ * Get teh start alpha value for when the activity is not swiped at all
+ *
+ * @return the start alpha value (0.0 to 1.0)
+ */
+ fun getScrimStartAlpha(): Float {
+ return scrimStartAlpha
+ }
+
+ fun setScrimStartAlpha(scrimStartAlpha: Float) {
+ this.scrimStartAlpha = scrimStartAlpha
+ }
+
+ /**
+ * Get the end alpha value for when the user almost swipes the activity off the screen
+ *
+ * @return the end alpha value (0.0 to 1.0)
+ */
+ fun getScrimEndAlpha(): Float {
+ return scrimEndAlpha
+ }
+
+ fun setScrimEndAlpha(scrimEndAlpha: Float) {
+ this.scrimEndAlpha = scrimEndAlpha
+ }
+
+ /**
+ * Get the velocity threshold at which the slide action is completed regardless of offset
+ * distance of the drag
+ *
+ * @return the velocity threshold
+ */
+ fun getVelocityThreshold(): Float {
+ return velocityThreshold
+ }
+
+ fun setVelocityThreshold(velocityThreshold: Float) {
+ this.velocityThreshold = velocityThreshold
+ }
+
+ /**
+ * Get at what % of the screen is the minimum viable distance the activity has to be dragged
+ * in-order to be slinged off the screen
+ *
+ * @return the distant threshold as a percentage of the screen size (width or height)
+ */
+ fun getDistanceThreshold(): Float {
+ return distanceThreshold
+ }
+
+ fun setDistanceThreshold(distanceThreshold: Float) {
+ this.distanceThreshold = distanceThreshold
+ }
+
+ fun getSensitivity(): Float {
+ return sensitivity
+ }
+
+ fun setSensitivity(sensitivity: Float) {
+ this.sensitivity = sensitivity
+ }
+
+ fun isAlphaForView(): Boolean {
+ return alphaForView
+ }
+
+ fun setAlphaForView(alphaForView: Boolean) {
+ this.alphaForView = alphaForView
+ }
+
+ /**
+ * Get the size of the edge field that is catchable
+ *
+ * @return the size of the edge that is grabable
+ * @see .isEdgeOnly
+ */
+ fun getEdgeSize(size: Float): Float {
+ return edgeSize * size
+ }
+
+ /**
+ * The Builder for this configuration class. This is the only way to create a
+ * configuration
+ */
+ class Builder {
+ private val config: SlidrConfig = SlidrConfig()
+ fun fromUnColoredToColoredStatusBar(en: Boolean): Builder {
+ config.fromUnColoredToColoredStatusBar = en
+ return this
+ }
+
+ fun position(position: SlidrPosition): Builder {
+ config.position = position
+ return this
+ }
+
+ fun scrimColor(@ColorInt color: Int): Builder {
+ config.scrimColor = color
+ return this
+ }
+
+ fun scrimStartAlpha(@FloatRange(from = 0.0, to = 1.0) alpha: Float): Builder {
+ config.scrimStartAlpha = alpha
+ return this
+ }
+
+ fun scrimEndAlpha(@FloatRange(from = 0.0, to = 1.0) alpha: Float): Builder {
+ config.scrimEndAlpha = alpha
+ return this
+ }
+
+ fun edge(flag: Boolean): Builder {
+ config.isEdgeOnly = flag
+ return this
+ }
+
+ fun edgeSize(@FloatRange(from = 0.0, to = 1.0) edgeSize: Float): Builder {
+ config.edgeSize = edgeSize
+ return this
+ }
+
+ fun ignoreChildScroll(ignoreChildScroll: Boolean): Builder {
+ config.isIgnoreChildScroll = ignoreChildScroll
+ return this
+ }
+
+ fun setAlphaForView(alphaForView: Boolean): Builder {
+ config.alphaForView = alphaForView
+ return this
+ }
+
+ fun listener(listener: SlidrListener?): Builder {
+ config.listener = listener
+ return this
+ }
+
+ fun build(): SlidrConfig {
+ val settings = get().other().slidrSettings
+ when (config.position) {
+ SlidrPosition.LEFT, SlidrPosition.RIGHT, SlidrPosition.HORIZONTAL -> {
+ config.sensitivity = settings.horizontal_sensitive
+ config.velocityThreshold = settings.horizontal_velocity_threshold
+ config.distanceThreshold = settings.horizontal_distance_threshold
+ }
+ SlidrPosition.TOP, SlidrPosition.BOTTOM, SlidrPosition.VERTICAL -> {
+ config.sensitivity = settings.vertical_sensitive
+ config.velocityThreshold = settings.vertical_velocity_threshold
+ config.distanceThreshold = settings.vertical_distance_threshold
+ }
+ }
+ return config
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrInterface.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrInterface.kt
new file mode 100644
index 000000000..7425775bf
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrInterface.kt
@@ -0,0 +1,6 @@
+package dev.ragnarok.fenrir.activity.slidr.model
+
+interface SlidrInterface {
+ fun lock()
+ fun unlock()
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrListener.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrListener.kt
new file mode 100644
index 000000000..592d3ad15
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrListener.kt
@@ -0,0 +1,10 @@
+package dev.ragnarok.fenrir.activity.slidr.model
+
+interface SlidrListener {
+
+ fun onSlideStateChanged(state: Int)
+ fun onSlideChange(percent: Float)
+ fun onSlideOpened()
+
+ fun onSlideClosed(): Boolean
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrListenerAdapter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrListenerAdapter.kt
new file mode 100644
index 000000000..1856ea209
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrListenerAdapter.kt
@@ -0,0 +1,10 @@
+package dev.ragnarok.fenrir.activity.slidr.model
+
+class SlidrListenerAdapter : SlidrListener {
+ override fun onSlideStateChanged(state: Int) {}
+ override fun onSlideChange(percent: Float) {}
+ override fun onSlideOpened() {}
+ override fun onSlideClosed(): Boolean {
+ return false
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrPosition.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrPosition.kt
new file mode 100644
index 000000000..a3260dfa0
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/model/SlidrPosition.kt
@@ -0,0 +1,5 @@
+package dev.ragnarok.fenrir.activity.slidr.model
+
+enum class SlidrPosition {
+ LEFT, RIGHT, TOP, BOTTOM, VERTICAL, HORIZONTAL
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/util/ViewHelper.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/util/ViewHelper.kt
new file mode 100644
index 000000000..368e10099
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/util/ViewHelper.kt
@@ -0,0 +1,125 @@
+package dev.ragnarok.fenrir.activity.slidr.util
+
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.WebView
+import android.widget.AbsListView
+import android.widget.HorizontalScrollView
+import android.widget.ScrollView
+import androidx.core.view.ScrollingView
+import androidx.viewpager2.widget.ViewPager2
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrPosition
+import dev.ragnarok.fenrir.view.TouchImageView
+import java.util.*
+
+object ViewHelper {
+ fun hasScrollableChildUnderPoint(
+ mView: View,
+ direction: SlidrPosition,
+ x: Int,
+ y: Int
+ ): Boolean {
+ val scrollableView = findScrollableViewContains(mView, direction, x, y)
+ return scrollableView != null
+ }
+
+ private fun findScrollableViewContains(
+ mView: View,
+ direction: SlidrPosition,
+ x: Int,
+ y: Int
+ ): View? {
+ if (isScrollableView(mView) && (canScroll(
+ mView,
+ direction
+ ) || mView is TouchImageView && mView.isZoomed)
+ ) {
+ return mView
+ }
+ if (mView !is ViewGroup) return null
+ val relativeX = x - mView.left + mView.scrollX
+ val relativeY = y - mView.top + mView.scrollY
+ for (i in 0 until mView.childCount) {
+ val childView = mView.getChildAt(i)
+ if (childView.visibility != View.VISIBLE || !isViewUnder(
+ childView,
+ relativeX,
+ relativeY
+ )
+ ) continue
+ val scrollableView =
+ findScrollableViewContains(childView, direction, relativeX, relativeY)
+ if (scrollableView != null) {
+ return scrollableView
+ }
+ }
+ return null
+ }
+
+ private fun canScroll(mView: View, direction: SlidrPosition): Boolean {
+ return when (direction) {
+ SlidrPosition.LEFT -> mView.canScrollHorizontally(-1)
+ SlidrPosition.RIGHT -> mView.canScrollHorizontally(1)
+ SlidrPosition.TOP -> mView.canScrollVertically(-1)
+ SlidrPosition.BOTTOM -> mView.canScrollVertically(1)
+ SlidrPosition.VERTICAL -> mView.canScrollVertically(-1) || mView.canScrollVertically(1)
+ SlidrPosition.HORIZONTAL -> mView.canScrollHorizontally(-1) || mView.canScrollHorizontally(
+ 1
+ )
+ }
+ }
+
+ private fun isScrollableView(mView: View): Boolean {
+ return (mView is ScrollView
+ || mView is HorizontalScrollView
+ || mView is AbsListView
+ || mView is ScrollingView
+ || mView is TouchImageView
+ || mView is ViewPager2
+ || mView is WebView)
+ }
+
+ private fun isViewUnder(view: View?, x: Int, y: Int): Boolean {
+ return if (view == null) {
+ false
+ } else x >= view.left && x < view.right && y >= view.top && y < view.bottom
+ }
+
+ private fun findScrollableInIterativeWay(
+ parent: View,
+ direction: SlidrPosition,
+ x: Int,
+ y: Int
+ ): View? {
+ val viewStack = Stack()
+ var viewInfo: ViewInfo? = ViewInfo(parent, x, y)
+ while (viewInfo != null) {
+ val mView = viewInfo.view
+ if (isScrollableView(mView) && (canScroll(
+ mView,
+ direction
+ ) || mView is TouchImageView && mView.isZoomed)
+ ) {
+ return mView
+ }
+ if (mView is ViewGroup) {
+ val relativeX = viewInfo.x - mView.left + mView.scrollX
+ val relativeY = viewInfo.y - mView.top + mView.scrollY
+ for (i in mView.childCount - 1 downTo 0) {
+ val childView = mView.getChildAt(i)
+ if (childView.visibility != View.VISIBLE || !isViewUnder(
+ childView,
+ relativeX,
+ relativeY
+ )
+ ) continue
+ viewStack.push(ViewInfo(childView, relativeX, relativeY))
+ }
+ }
+ viewInfo = if (viewStack.isEmpty()) null else viewStack.pop()
+ }
+ return null
+ }
+
+ internal class ViewInfo(val view: View, val x: Int, val y: Int)
+}
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/widget/ScrimRenderer.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/widget/ScrimRenderer.kt
new file mode 100644
index 000000000..b9a6c0208
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/widget/ScrimRenderer.kt
@@ -0,0 +1,90 @@
+package dev.ragnarok.fenrir.activity.slidr.widget
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Rect
+import android.view.View
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrPosition
+
+internal class ScrimRenderer(private val rootView: View, private val decorView: View) {
+ private val dirtyRect: Rect = Rect()
+ fun render(canvas: Canvas, position: SlidrPosition?, paint: Paint) {
+ when (position) {
+ SlidrPosition.LEFT -> renderLeft(canvas, paint)
+ SlidrPosition.RIGHT -> renderRight(canvas, paint)
+ SlidrPosition.TOP -> renderTop(canvas, paint)
+ SlidrPosition.BOTTOM -> renderBottom(canvas, paint)
+ SlidrPosition.VERTICAL -> renderVertical(canvas, paint)
+ SlidrPosition.HORIZONTAL -> renderHorizontal(canvas, paint)
+ else -> {}
+ }
+ }
+
+ fun getDirtyRect(position: SlidrPosition?): Rect {
+ when (position) {
+ SlidrPosition.LEFT -> dirtyRect[0, 0, decorView.left] = rootView.measuredHeight
+ SlidrPosition.RIGHT -> dirtyRect[decorView.right, 0, rootView.measuredWidth] =
+ rootView.measuredHeight
+ SlidrPosition.TOP -> dirtyRect[0, 0, rootView.measuredWidth] = decorView.top
+ SlidrPosition.BOTTOM -> dirtyRect[0, decorView.bottom, rootView.measuredWidth] =
+ rootView.measuredHeight
+ SlidrPosition.VERTICAL -> if (decorView.top > 0) {
+ dirtyRect[0, 0, rootView.measuredWidth] = decorView.top
+ } else {
+ dirtyRect[0, decorView.bottom, rootView.measuredWidth] = rootView.measuredHeight
+ }
+ SlidrPosition.HORIZONTAL -> if (decorView.left > 0) {
+ dirtyRect[0, 0, decorView.left] = rootView.measuredHeight
+ } else {
+ dirtyRect[decorView.right, 0, rootView.measuredWidth] = rootView.measuredHeight
+ }
+ else -> {}
+ }
+ return dirtyRect
+ }
+
+ private fun renderLeft(canvas: Canvas, paint: Paint) {
+ canvas.drawRect(0f, 0f, decorView.left.toFloat(), rootView.measuredHeight.toFloat(), paint)
+ }
+
+ private fun renderRight(canvas: Canvas, paint: Paint) {
+ canvas.drawRect(
+ decorView.right.toFloat(),
+ 0f,
+ rootView.measuredWidth.toFloat(),
+ rootView.measuredHeight.toFloat(),
+ paint
+ )
+ }
+
+ private fun renderTop(canvas: Canvas, paint: Paint) {
+ canvas.drawRect(0f, 0f, rootView.measuredWidth.toFloat(), decorView.top.toFloat(), paint)
+ }
+
+ private fun renderBottom(canvas: Canvas, paint: Paint) {
+ canvas.drawRect(
+ 0f,
+ decorView.bottom.toFloat(),
+ rootView.measuredWidth.toFloat(),
+ rootView.measuredHeight.toFloat(),
+ paint
+ )
+ }
+
+ private fun renderVertical(canvas: Canvas, paint: Paint) {
+ if (decorView.top > 0) {
+ renderTop(canvas, paint)
+ } else {
+ renderBottom(canvas, paint)
+ }
+ }
+
+ private fun renderHorizontal(canvas: Canvas, paint: Paint) {
+ if (decorView.left > 0) {
+ renderLeft(canvas, paint)
+ } else {
+ renderRight(canvas, paint)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/widget/SliderPanel.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/widget/SliderPanel.kt
new file mode 100644
index 000000000..625a03f09
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/slidr/widget/SliderPanel.kt
@@ -0,0 +1,711 @@
+package dev.ragnarok.fenrir.activity.slidr.widget
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.view.MotionEvent
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.view.ViewCompat
+import androidx.customview.widget.ViewDragHelper
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrInterface
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrPosition
+import dev.ragnarok.fenrir.activity.slidr.util.ViewHelper.hasScrollableChildUnderPoint
+import kotlin.math.abs
+
+class SliderPanel : FrameLayout {
+ private var screenWidth = 0
+ private var screenHeight = 0
+ private var decorView: View? = null
+ private var dragHelper: ViewDragHelper? = null
+ private var listener: OnPanelSlideListener? = null
+ private lateinit var scrimPaint: Paint
+ private var scrimRenderer: ScrimRenderer? = null
+ private var isLocked = false
+
+ /**
+ * Get the default [SlidrInterface] from which to control the panel with after attachment
+ */
+ val defaultInterface: SlidrInterface = object : SlidrInterface {
+ override fun lock() {
+ this@SliderPanel.lock()
+ }
+
+ override fun unlock() {
+ this@SliderPanel.unlock()
+ }
+ }
+ private var isEdgeTouched = false
+ private var edgePosition = 0
+ private var config: SlidrConfig = SlidrConfig.Builder().build()
+ private var startX = 0f
+ private var startY = 0f
+
+ /**
+ * The drag helper callback interface for the Left position
+ */
+ private val leftCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
+ override fun tryCaptureView(child: View, pointerId: Int): Boolean {
+ val canChildScroll = !config.isIgnoreChildScroll && hasScrollableChildUnderPoint(
+ child,
+ SlidrPosition.LEFT,
+ startX.toInt(),
+ startY.toInt()
+ )
+ val edgeCase =
+ !config.isEdgeOnly || dragHelper?.isEdgeTouched(edgePosition, pointerId) == true
+ return !canChildScroll && child.id == decorView?.id && edgeCase
+ }
+
+ override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
+ return clamp(left, 0, screenWidth)
+ }
+
+ override fun getViewHorizontalDragRange(child: View): Int {
+ return screenWidth
+ }
+
+ override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
+ super.onViewReleased(releasedChild, xvel, yvel)
+ val left = releasedChild.left
+ var settleLeft = 0
+ val leftThreshold = (width * config.getDistanceThreshold()).toInt()
+ val isVerticalSwiping = abs(yvel) > config.getVelocityThreshold()
+ if (xvel > 0) {
+ if (abs(xvel) > config.getVelocityThreshold() && !isVerticalSwiping) {
+ settleLeft = screenWidth
+ } else if (left > leftThreshold) {
+ settleLeft = screenWidth
+ }
+ } else if (xvel == 0f) {
+ if (left > leftThreshold) {
+ settleLeft = screenWidth
+ }
+ }
+ dragHelper?.settleCapturedViewAt(settleLeft, releasedChild.top)
+ invalidate()
+ }
+
+ override fun onViewPositionChanged(
+ changedView: View,
+ left: Int,
+ top: Int,
+ dx: Int,
+ dy: Int
+ ) {
+ super.onViewPositionChanged(changedView, left, top, dx, dy)
+ val percent = 1f - left.toFloat() / screenWidth.toFloat()
+ listener?.onSlideChange(percent)
+
+ // Update the dimmer alpha
+ applyScrim(percent)
+ }
+
+ override fun onViewDragStateChanged(state: Int) {
+ super.onViewDragStateChanged(state)
+ listener?.onStateChanged(state)
+ when (state) {
+ ViewDragHelper.STATE_IDLE -> if (decorView?.left == 0) {
+ // State Open
+ listener?.onOpened()
+ } else {
+ // State Closed
+ listener?.onClosed()
+ }
+ ViewDragHelper.STATE_DRAGGING, ViewDragHelper.STATE_SETTLING -> {}
+ }
+ }
+ }
+
+ /**
+ * The drag helper callbacks for dragging the slidr attachment from the right of the screen
+ */
+ private val rightCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
+ override fun tryCaptureView(child: View, pointerId: Int): Boolean {
+ val canChildScroll = !config.isIgnoreChildScroll && hasScrollableChildUnderPoint(
+ child,
+ SlidrPosition.RIGHT,
+ startX.toInt(),
+ startY.toInt()
+ )
+ val edgeCase =
+ !config.isEdgeOnly || dragHelper?.isEdgeTouched(edgePosition, pointerId) == true
+ return !canChildScroll && child.id == decorView?.id && edgeCase
+ }
+
+ override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
+ return clamp(left, -screenWidth, 0)
+ }
+
+ override fun getViewHorizontalDragRange(child: View): Int {
+ return screenWidth
+ }
+
+ override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
+ super.onViewReleased(releasedChild, xvel, yvel)
+ val left = releasedChild.left
+ var settleLeft = 0
+ val leftThreshold = (width * config.getDistanceThreshold()).toInt()
+ val isVerticalSwiping = abs(yvel) > config.getVelocityThreshold()
+ if (xvel < 0) {
+ if (abs(xvel) > config.getVelocityThreshold() && !isVerticalSwiping) {
+ settleLeft = -screenWidth
+ } else if (left < -leftThreshold) {
+ settleLeft = -screenWidth
+ }
+ } else if (xvel == 0f) {
+ if (left < -leftThreshold) {
+ settleLeft = -screenWidth
+ }
+ }
+ dragHelper?.settleCapturedViewAt(settleLeft, releasedChild.top)
+ invalidate()
+ }
+
+ override fun onViewPositionChanged(
+ changedView: View,
+ left: Int,
+ top: Int,
+ dx: Int,
+ dy: Int
+ ) {
+ super.onViewPositionChanged(changedView, left, top, dx, dy)
+ val percent = 1f - abs(left)
+ .toFloat() / screenWidth.toFloat()
+ listener?.onSlideChange(percent)
+
+ // Update the dimmer alpha
+ applyScrim(percent)
+ }
+
+ override fun onViewDragStateChanged(state: Int) {
+ super.onViewDragStateChanged(state)
+ listener?.onStateChanged(state)
+ when (state) {
+ ViewDragHelper.STATE_IDLE -> if (decorView?.left == 0) {
+ // State Open
+ listener?.onOpened()
+ } else {
+ // State Closed
+ listener?.onClosed()
+ }
+ ViewDragHelper.STATE_DRAGGING, ViewDragHelper.STATE_SETTLING -> {}
+ }
+ }
+ }
+
+ /**
+ * The drag helper callbacks for dragging the slidr attachment from the top of the screen
+ */
+ private val topCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
+ override fun tryCaptureView(child: View, pointerId: Int): Boolean {
+ val canChildScroll = !config.isIgnoreChildScroll && hasScrollableChildUnderPoint(
+ child,
+ SlidrPosition.TOP,
+ startX.toInt(),
+ startY.toInt()
+ )
+ return !canChildScroll && child.id == decorView?.id && (!config.isEdgeOnly || isEdgeTouched)
+ }
+
+ override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
+ return clamp(top, 0, screenHeight)
+ }
+
+ override fun getViewVerticalDragRange(child: View): Int {
+ return screenHeight
+ }
+
+ override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
+ super.onViewReleased(releasedChild, xvel, yvel)
+ val top = releasedChild.top
+ var settleTop = 0
+ val topThreshold = (height * config.getDistanceThreshold()).toInt()
+ val isSideSwiping = abs(xvel) > config.getVelocityThreshold()
+ if (yvel > 0) {
+ if (abs(yvel) > config.getVelocityThreshold() && !isSideSwiping) {
+ settleTop = screenHeight
+ } else if (top > topThreshold) {
+ settleTop = screenHeight
+ }
+ } else if (yvel == 0f) {
+ if (top > topThreshold) {
+ settleTop = screenHeight
+ }
+ }
+ dragHelper?.settleCapturedViewAt(releasedChild.left, settleTop)
+ invalidate()
+ }
+
+ override fun onViewPositionChanged(
+ changedView: View,
+ left: Int,
+ top: Int,
+ dx: Int,
+ dy: Int
+ ) {
+ super.onViewPositionChanged(changedView, left, top, dx, dy)
+ val percent = 1f - abs(top)
+ .toFloat() / screenHeight.toFloat()
+ listener?.onSlideChange(percent)
+
+ // Update the dimmer alpha
+ applyScrim(percent)
+ }
+
+ override fun onViewDragStateChanged(state: Int) {
+ super.onViewDragStateChanged(state)
+ listener?.onStateChanged(state)
+ when (state) {
+ ViewDragHelper.STATE_IDLE -> if (decorView?.top == 0) {
+ // State Open
+ listener?.onOpened()
+ } else {
+ // State Closed
+ listener?.onClosed()
+ }
+ ViewDragHelper.STATE_DRAGGING, ViewDragHelper.STATE_SETTLING -> {}
+ }
+ }
+ }
+
+ /**
+ * The drag helper callbacks for dragging the slidr attachment from the bottom of hte screen
+ */
+ private val bottomCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
+ override fun tryCaptureView(child: View, pointerId: Int): Boolean {
+ val canChildScroll = !config.isIgnoreChildScroll && hasScrollableChildUnderPoint(
+ child,
+ SlidrPosition.BOTTOM,
+ startX.toInt(),
+ startY.toInt()
+ )
+ return !canChildScroll && child.id == decorView?.id && (!config.isEdgeOnly || isEdgeTouched)
+ }
+
+ override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
+ return clamp(top, -screenHeight, 0)
+ }
+
+ override fun getViewVerticalDragRange(child: View): Int {
+ return screenHeight
+ }
+
+ override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
+ super.onViewReleased(releasedChild, xvel, yvel)
+ val top = releasedChild.top
+ var settleTop = 0
+ val topThreshold = (height * config.getDistanceThreshold()).toInt()
+ val isSideSwiping = abs(xvel) > config.getVelocityThreshold()
+ if (yvel < 0) {
+ if (abs(yvel) > config.getVelocityThreshold() && !isSideSwiping) {
+ settleTop = -screenHeight
+ } else if (top < -topThreshold) {
+ settleTop = -screenHeight
+ }
+ } else if (yvel == 0f) {
+ if (top < -topThreshold) {
+ settleTop = -screenHeight
+ }
+ }
+ dragHelper?.settleCapturedViewAt(releasedChild.left, settleTop)
+ invalidate()
+ }
+
+ override fun onViewPositionChanged(
+ changedView: View,
+ left: Int,
+ top: Int,
+ dx: Int,
+ dy: Int
+ ) {
+ super.onViewPositionChanged(changedView, left, top, dx, dy)
+ val percent = 1f - abs(top)
+ .toFloat() / screenHeight.toFloat()
+ listener?.onSlideChange(percent)
+
+ // Update the dimmer alpha
+ applyScrim(percent)
+ }
+
+ override fun onViewDragStateChanged(state: Int) {
+ super.onViewDragStateChanged(state)
+ listener?.onStateChanged(state)
+ when (state) {
+ ViewDragHelper.STATE_IDLE -> if (decorView?.top == 0) {
+ // State Open
+ listener?.onOpened()
+ } else {
+ // State Closed
+ listener?.onClosed()
+ }
+ ViewDragHelper.STATE_DRAGGING, ViewDragHelper.STATE_SETTLING -> {}
+ }
+ }
+ }
+
+ /**
+ * The drag helper callbacks for dragging the slidr attachment in both vertical directions
+ */
+ private val verticalCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
+ override fun tryCaptureView(child: View, pointerId: Int): Boolean {
+ return child.id == decorView?.id && (!config.isEdgeOnly || isEdgeTouched)
+ }
+
+ override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
+ if (top > 0 && dy > 0 || top < 0 && dy < 0) {
+ val slidrPosition = if (dy > 0) SlidrPosition.TOP else SlidrPosition.BOTTOM
+ val canChildScroll = !config.isIgnoreChildScroll && hasScrollableChildUnderPoint(
+ child,
+ slidrPosition,
+ startX.toInt(),
+ startY.toInt()
+ )
+ if (canChildScroll) {
+ return 0
+ }
+ }
+ return clamp(top, -screenHeight, screenHeight)
+ }
+
+ override fun getViewVerticalDragRange(child: View): Int {
+ return screenHeight
+ }
+
+ override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
+ super.onViewReleased(releasedChild, xvel, yvel)
+ val top = releasedChild.top
+ var settleTop = 0
+ val topThreshold = (height * config.getDistanceThreshold()).toInt()
+ val isSideSwiping = abs(xvel) > config.getVelocityThreshold()
+ if (yvel > 0) {
+
+ // Being slinged down
+ if (abs(yvel) > config.getVelocityThreshold() && !isSideSwiping) {
+ settleTop = screenHeight
+ } else if (top > topThreshold) {
+ settleTop = screenHeight
+ }
+ } else if (yvel < 0) {
+ // Being slinged up
+ if (abs(yvel) > config.getVelocityThreshold() && !isSideSwiping) {
+ settleTop = -screenHeight
+ } else if (top < -topThreshold) {
+ settleTop = -screenHeight
+ }
+ } else {
+ if (top > topThreshold) {
+ settleTop = screenHeight
+ } else if (top < -topThreshold) {
+ settleTop = -screenHeight
+ }
+ }
+ dragHelper?.settleCapturedViewAt(releasedChild.left, settleTop)
+ invalidate()
+ }
+
+ override fun onViewPositionChanged(
+ changedView: View,
+ left: Int,
+ top: Int,
+ dx: Int,
+ dy: Int
+ ) {
+ super.onViewPositionChanged(changedView, left, top, dx, dy)
+ val percent = 1f - abs(top)
+ .toFloat() / screenHeight.toFloat()
+ listener?.onSlideChange(percent)
+
+ // Update the dimmer alpha
+ applyScrim(percent)
+ }
+
+ override fun onViewDragStateChanged(state: Int) {
+ super.onViewDragStateChanged(state)
+ listener?.onStateChanged(state)
+ when (state) {
+ ViewDragHelper.STATE_IDLE -> if (decorView?.top == 0) {
+ // State Open
+ listener?.onOpened()
+ } else {
+ // State Closed
+ listener?.onClosed()
+ }
+ ViewDragHelper.STATE_DRAGGING, ViewDragHelper.STATE_SETTLING -> {}
+ }
+ }
+ }
+
+ /**
+ * The drag helper callbacks for dragging the slidr attachment in both horizontal directions
+ */
+ private val horizontalCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
+ override fun tryCaptureView(child: View, pointerId: Int): Boolean {
+ val edgeCase =
+ !config.isEdgeOnly || dragHelper?.isEdgeTouched(edgePosition, pointerId) == true
+ return child.id == decorView?.id && edgeCase
+ }
+
+ override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
+ if (left > 0 && dx > 0 || left < 0 && dx < 0) {
+ val slidrPosition = if (dx > 0) SlidrPosition.LEFT else SlidrPosition.RIGHT
+ val canChildScroll = !config.isIgnoreChildScroll && hasScrollableChildUnderPoint(
+ child,
+ slidrPosition,
+ startX.toInt(),
+ startY.toInt()
+ )
+ if (canChildScroll) {
+ return 0
+ }
+ }
+ return clamp(left, -screenWidth, screenWidth)
+ }
+
+ override fun getViewHorizontalDragRange(child: View): Int {
+ return screenWidth
+ }
+
+ override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
+ super.onViewReleased(releasedChild, xvel, yvel)
+ val left = releasedChild.left
+ var settleLeft = 0
+ val leftThreshold = (width * config.getDistanceThreshold()).toInt()
+ val isVerticalSwiping = abs(yvel) > config.getVelocityThreshold()
+ if (xvel > 0) {
+ if (abs(xvel) > config.getVelocityThreshold() && !isVerticalSwiping) {
+ settleLeft = screenWidth
+ } else if (left > leftThreshold) {
+ settleLeft = screenWidth
+ }
+ } else if (xvel < 0) {
+ if (abs(xvel) > config.getVelocityThreshold() && !isVerticalSwiping) {
+ settleLeft = -screenWidth
+ } else if (left < -leftThreshold) {
+ settleLeft = -screenWidth
+ }
+ } else {
+ if (left > leftThreshold) {
+ settleLeft = screenWidth
+ } else if (left < -leftThreshold) {
+ settleLeft = -screenWidth
+ }
+ }
+ dragHelper?.settleCapturedViewAt(settleLeft, releasedChild.top)
+ invalidate()
+ }
+
+ override fun onViewPositionChanged(
+ changedView: View,
+ left: Int,
+ top: Int,
+ dx: Int,
+ dy: Int
+ ) {
+ super.onViewPositionChanged(changedView, left, top, dx, dy)
+ val percent = 1f - abs(left)
+ .toFloat() / screenWidth.toFloat()
+ listener?.onSlideChange(percent)
+
+ // Update the dimmer alpha
+ applyScrim(percent)
+ }
+
+ override fun onViewDragStateChanged(state: Int) {
+ super.onViewDragStateChanged(state)
+ listener?.onStateChanged(state)
+ when (state) {
+ ViewDragHelper.STATE_IDLE -> if (decorView?.left == 0) {
+ // State Open
+ listener?.onOpened()
+ } else {
+ // State Closed
+ listener?.onClosed()
+ }
+ ViewDragHelper.STATE_DRAGGING, ViewDragHelper.STATE_SETTLING -> {}
+ }
+ }
+ }
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, decorView: View?, config: SlidrConfig?) : super(
+ context
+ ) {
+ this.decorView = decorView
+ this.config = config ?: SlidrConfig.Builder().build()
+ init()
+ }
+
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
+ if (isLocked) {
+ return false
+ }
+ startX = ev.x
+ startY = ev.y
+ if (config.isEdgeOnly) {
+ isEdgeTouched = canDragFromEdge(ev)
+ }
+
+ // Fix for pull request #13 and issue #12
+ val interceptForDrag: Boolean = try {
+ dragHelper?.shouldInterceptTouchEvent(ev) == true
+ } catch (e: Exception) {
+ false
+ }
+ return interceptForDrag && !isLocked
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ if (isLocked) {
+ return false
+ }
+ try {
+ dragHelper?.processTouchEvent(event)
+ } catch (e: IllegalArgumentException) {
+ return false
+ }
+ return true
+ }
+
+ override fun computeScroll() {
+ super.computeScroll()
+ if (dragHelper?.continueSettling(true) == true) {
+ ViewCompat.postInvalidateOnAnimation(this)
+ }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ scrimRenderer?.render(canvas, config.position, scrimPaint)
+ }
+
+ /**
+ * Set the panel slide listener that gets called based on slider changes
+ *
+ * @param listener callback implementation
+ */
+ fun setOnPanelSlideListener(listener: OnPanelSlideListener?) {
+ this.listener = listener
+ }
+
+ private fun init() {
+ setWillNotDraw(false)
+ screenWidth = resources.displayMetrics.widthPixels
+ val density = resources.displayMetrics.density
+ val minVel = MIN_FLING_VELOCITY * density
+ val callback: ViewDragHelper.Callback
+ when (config.position) {
+ SlidrPosition.RIGHT -> {
+ callback = rightCallback
+ edgePosition = ViewDragHelper.EDGE_RIGHT
+ }
+ SlidrPosition.TOP -> {
+ callback = topCallback
+ edgePosition = ViewDragHelper.EDGE_TOP
+ }
+ SlidrPosition.BOTTOM -> {
+ callback = bottomCallback
+ edgePosition = ViewDragHelper.EDGE_BOTTOM
+ }
+ SlidrPosition.VERTICAL -> {
+ callback = verticalCallback
+ edgePosition = ViewDragHelper.EDGE_TOP or ViewDragHelper.EDGE_BOTTOM
+ }
+ SlidrPosition.HORIZONTAL -> {
+ callback = horizontalCallback
+ edgePosition = ViewDragHelper.EDGE_LEFT or ViewDragHelper.EDGE_RIGHT
+ }
+ else -> {
+ callback = leftCallback
+ edgePosition = ViewDragHelper.EDGE_LEFT
+ }
+ }
+ dragHelper = ViewDragHelper.create(this, config.getSensitivity(), callback)
+ dragHelper?.minVelocity = minVel
+ dragHelper?.setEdgeTrackingEnabled(edgePosition)
+ isMotionEventSplittingEnabled = false
+
+ // Setup the dimmer view
+ scrimPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ if (config.getScrimColor() >= 0) {
+ scrimPaint.color = config.getScrimColor()
+ scrimPaint.alpha = toAlpha(config.getScrimStartAlpha())
+ } else {
+ scrimPaint.color = Color.BLACK
+ scrimPaint.alpha = 0
+ }
+ decorView?.let {
+ scrimRenderer = ScrimRenderer(this, it)
+ }
+
+ /*
+ * This is so we can get the height of the view and
+ * ignore the system navigation that would be included if we
+ * retrieved this value from the DisplayMetrics
+ */post { screenHeight = height }
+ }
+
+ internal fun lock() {
+ dragHelper?.abort()
+ isLocked = true
+ }
+
+ internal fun unlock() {
+ dragHelper?.abort()
+ isLocked = false
+ }
+
+ private fun canDragFromEdge(ev: MotionEvent): Boolean {
+ val x = ev.x
+ val y = ev.y
+ return when (config.position) {
+ SlidrPosition.LEFT -> x < config.getEdgeSize(width.toFloat())
+ SlidrPosition.RIGHT -> x > width - config.getEdgeSize(width.toFloat())
+ SlidrPosition.BOTTOM -> y > height - config.getEdgeSize(height.toFloat())
+ SlidrPosition.TOP -> y < config.getEdgeSize(height.toFloat())
+ SlidrPosition.HORIZONTAL -> x < config.getEdgeSize(width.toFloat()) || x > width - config.getEdgeSize(
+ width.toFloat()
+ )
+ SlidrPosition.VERTICAL -> y < config.getEdgeSize(height.toFloat()) || y > height - config.getEdgeSize(
+ height.toFloat()
+ )
+ }
+ }
+
+ internal fun applyScrim(percent: Float) {
+ if (config.getScrimColor() < 0) {
+ return
+ }
+ val alpha =
+ percent * (config.getScrimStartAlpha() - config.getScrimEndAlpha()) + config.getScrimEndAlpha()
+ scrimPaint.alpha = toAlpha(alpha)
+ //invalidate(scrimRenderer.getDirtyRect(config.getPosition()));
+ invalidate()
+ }
+
+ /**
+ * The panel sliding interface that gets called
+ * whenever the panel is closed or opened
+ */
+ interface OnPanelSlideListener {
+ fun onStateChanged(state: Int)
+ fun onClosed()
+ fun onOpened()
+ fun onSlideChange(percent: Float)
+ }
+
+ companion object {
+ private const val MIN_FLING_VELOCITY = 400 // dips per second
+ internal fun clamp(value: Int, min: Int, max: Int): Int {
+ return min.coerceAtLeast(max.coerceAtMost(value))
+ }
+
+ internal fun toAlpha(percentage: Float): Int {
+ return (percentage * 255).toInt()
+ }
+ }
+}
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/storypager/IStoryPagerView.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/storypager/IStoryPagerView.kt
new file mode 100644
index 000000000..c96c8fb02
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/storypager/IStoryPagerView.kt
@@ -0,0 +1,21 @@
+package dev.ragnarok.fenrir.activity.storypager
+
+import androidx.annotation.StringRes
+import dev.ragnarok.fenrir.fragment.base.core.IErrorView
+import dev.ragnarok.fenrir.fragment.base.core.IMvpView
+import dev.ragnarok.fenrir.fragment.base.core.IToastView
+import dev.ragnarok.fenrir.media.story.IStoryPlayer
+import dev.ragnarok.fenrir.model.Story
+
+interface IStoryPagerView : IMvpView, IErrorView, IToastView {
+ fun displayData(pageCount: Int, selectedIndex: Int)
+ fun setAspectRatioAt(position: Int, w: Int, h: Int)
+ fun setPreparingProgressVisible(position: Int, preparing: Boolean)
+ fun attachDisplayToPlayer(adapterPosition: Int, storyPlayer: IStoryPlayer?)
+ fun setToolbarTitle(@StringRes titleRes: Int, vararg params: Any?)
+ fun setToolbarSubtitle(story: Story, account_id: Int)
+ fun onShare(story: Story, account_id: Int)
+ fun configHolder(adapterPosition: Int, progress: Boolean, aspectRatioW: Int, aspectRatioH: Int)
+ fun onNext()
+ fun requestWriteExternalStoragePermission()
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/storypager/StoryPagerActivity.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/storypager/StoryPagerActivity.kt
new file mode 100644
index 000000000..e929cfcc7
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/storypager/StoryPagerActivity.kt
@@ -0,0 +1,686 @@
+package dev.ragnarok.fenrir.activity.storypager
+
+import android.Manifest
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.util.SparseArray
+import android.view.*
+import android.widget.ImageView
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.annotation.IdRes
+import androidx.annotation.LayoutRes
+import androidx.annotation.StringRes
+import androidx.appcompat.widget.Toolbar
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.squareup.picasso3.Callback
+import com.squareup.picasso3.Transformation
+import dev.ragnarok.fenrir.*
+import dev.ragnarok.fenrir.activity.ActivityFeatures
+import dev.ragnarok.fenrir.activity.BaseMvpActivity
+import dev.ragnarok.fenrir.activity.SendAttachmentsActivity
+import dev.ragnarok.fenrir.activity.slidr.Slidr
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrConfig
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrListener
+import dev.ragnarok.fenrir.activity.slidr.model.SlidrPosition
+import dev.ragnarok.fenrir.fragment.audio.AudioPlayerFragment
+import dev.ragnarok.fenrir.fragment.base.core.IPresenterFactory
+import dev.ragnarok.fenrir.link.LinkHelper
+import dev.ragnarok.fenrir.listener.AppStyleable
+import dev.ragnarok.fenrir.media.story.IStoryPlayer
+import dev.ragnarok.fenrir.model.PhotoSize
+import dev.ragnarok.fenrir.model.Story
+import dev.ragnarok.fenrir.module.FenrirNative
+import dev.ragnarok.fenrir.module.parcel.ParcelFlags
+import dev.ragnarok.fenrir.module.parcel.ParcelNative
+import dev.ragnarok.fenrir.picasso.PicassoInstance
+import dev.ragnarok.fenrir.place.Place
+import dev.ragnarok.fenrir.place.PlaceFactory
+import dev.ragnarok.fenrir.place.PlaceProvider
+import dev.ragnarok.fenrir.settings.CurrentTheme
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.util.AppPerms.requestPermissionsAbs
+import dev.ragnarok.fenrir.util.HelperSimple
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.ViewUtils
+import dev.ragnarok.fenrir.util.rxutils.RxUtils
+import dev.ragnarok.fenrir.util.toast.CustomToast.Companion.createCustomToast
+import dev.ragnarok.fenrir.view.CircleCounterButton
+import dev.ragnarok.fenrir.view.ExpandableSurfaceView
+import dev.ragnarok.fenrir.view.TouchImageView
+import dev.ragnarok.fenrir.view.natives.rlottie.RLottieImageView
+import dev.ragnarok.fenrir.view.pager.WeakPicassoLoadCallback
+import io.reactivex.rxjava3.core.Completable
+import io.reactivex.rxjava3.disposables.Disposable
+import java.lang.ref.WeakReference
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+class StoryPagerActivity : BaseMvpActivity(),
+ IStoryPagerView, PlaceProvider, AppStyleable {
+ private val mHolderSparseArray = SparseArray>()
+ private var mViewPager: ViewPager2? = null
+ private var mToolbar: Toolbar? = null
+ private var mAvatar: ImageView? = null
+ private var mExp: TextView? = null
+ private var transformation: Transformation? = null
+ private var mDownload: CircleCounterButton? = null
+ private var mShare: CircleCounterButton? = null
+ private var mLink: CircleCounterButton? = null
+ private var mFullscreen = false
+ private var hasExternalUrl = false
+ private var helpDisposable = Disposable.disposed()
+
+ @LayoutRes
+ override fun getNoMainContentView(): Int {
+ return R.layout.fragment_story_pager
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ mFullscreen = savedInstanceState?.getBoolean("mFullscreen") ?: false
+ transformation = CurrentTheme.createTransformationForAvatar()
+ val mContentRoot = findViewById(R.id.story_pager_root)
+ mToolbar = findViewById(R.id.toolbar)
+ setSupportActionBar(mToolbar)
+ mAvatar = findViewById(R.id.toolbar_avatar)
+ mViewPager = findViewById(R.id.view_pager)
+ mViewPager?.offscreenPageLimit = 1
+ mViewPager?.setPageTransformer(
+ Utils.createPageTransform(
+ Settings.get().main().viewpager_page_transform
+ )
+ )
+ mExp = findViewById(R.id.item_story_expires)
+ val mHelper = findViewById(R.id.swipe_helper)
+ if (HelperSimple.needHelp(HelperSimple.STORY_HELPER, 2)) {
+ mHelper?.visibility = View.VISIBLE
+ mHelper?.fromRes(
+ dev.ragnarok.fenrir_common.R.raw.story_guide_hand_swipe,
+ Utils.dp(500F),
+ Utils.dp(500F),
+ intArrayOf(0x333333, CurrentTheme.getColorSecondary(this))
+ )
+ mHelper?.playAnimation()
+ helpDisposable = Completable.create {
+ it.onComplete()
+ }.delay(5, TimeUnit.SECONDS).fromIOToMain().subscribe({
+ mHelper?.clearAnimationDrawable()
+ mHelper?.visibility = View.GONE
+ }, RxUtils.ignore())
+ } else {
+ mHelper?.visibility = View.GONE
+ }
+ mViewPager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+ override fun onPageSelected(position: Int) {
+ super.onPageSelected(position)
+ presenter?.firePageSelected(position)
+ }
+ })
+ mDownload = findViewById(R.id.button_download)
+ mShare = findViewById(R.id.button_share)
+ mShare?.setOnClickListener { presenter?.fireShareButtonClick() }
+ mDownload?.setOnClickListener { presenter?.fireDownloadButtonClick() }
+ mLink = findViewById(R.id.button_link)
+ resolveFullscreenViews()
+ val mButtonsRoot: View = findViewById(R.id.buttons)
+
+ Slidr.attach(
+ this,
+ SlidrConfig.Builder().setAlphaForView(false).fromUnColoredToColoredStatusBar(true)
+ .position(SlidrPosition.LEFT)
+ .listener(object : SlidrListener {
+ override fun onSlideStateChanged(state: Int) {
+
+ }
+
+ @SuppressLint("Range")
+ override fun onSlideChange(percent: Float) {
+ var tmp = 1f - percent
+ tmp *= 4
+ tmp = Utils.clamp(1f - tmp, 0f, 1f)
+ if (Utils.hasOreo()) {
+ mContentRoot?.setBackgroundColor(Color.argb(tmp, 0f, 0f, 0f))
+ } else {
+ mContentRoot?.setBackgroundColor(
+ Color.argb(
+ (tmp * 255).toInt(),
+ 0,
+ 0,
+ 0
+ )
+ )
+ }
+ mButtonsRoot.alpha = tmp
+ mToolbar?.alpha = tmp
+ mViewPager?.alpha = Utils.clamp(percent, 0f, 1f)
+ }
+
+ override fun onSlideOpened() {
+
+ }
+
+ override fun onSlideClosed(): Boolean {
+ finish()
+ overridePendingTransition(0, 0)
+ return true
+ }
+
+ }).build()
+ )
+ }
+
+ override fun openPlace(place: Place) {
+ val args = place.safeArguments()
+ when (place.type) {
+ Place.PLAYER -> {
+ val player = supportFragmentManager.findFragmentByTag("audio_player")
+ if (player is AudioPlayerFragment) player.dismiss()
+ AudioPlayerFragment.newInstance(args).show(supportFragmentManager, "audio_player")
+ }
+ else -> Utils.openPlaceWithSwipebleActivity(this, place)
+ }
+ }
+
+ override fun hideMenu(hide: Boolean) {}
+ override fun openMenu(open: Boolean) {}
+
+ @Suppress("DEPRECATION")
+ override fun setStatusbarColored(colored: Boolean, invertIcons: Boolean) {
+ val statusbarNonColored = CurrentTheme.getStatusBarNonColored(this)
+ val statusbarColored = CurrentTheme.getStatusBarColor(this)
+ val w = window
+ w.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ w.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ w.statusBarColor = if (colored) statusbarColored else statusbarNonColored
+ @ColorInt val navigationColor =
+ if (colored) CurrentTheme.getNavigationBarColor(this) else Color.BLACK
+ w.navigationBarColor = navigationColor
+ if (Utils.hasMarshmallow()) {
+ var flags = window.decorView.systemUiVisibility
+ flags = if (invertIcons) {
+ flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ } else {
+ flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ }
+ window.decorView.systemUiVisibility = flags
+ }
+ if (Utils.hasOreo()) {
+ var flags = window.decorView.systemUiVisibility
+ if (invertIcons) {
+ flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ w.decorView.systemUiVisibility = flags
+ w.navigationBarColor = Color.WHITE
+ } else {
+ flags = flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ w.decorView.systemUiVisibility = flags
+ }
+ }
+ }
+
+ private val requestWritePermission = requestPermissionsAbs(
+ arrayOf(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ )
+ ) {
+ lazyPresenter { fireWritePermissionResolved() }
+ }
+
+ override fun requestWriteExternalStoragePermission() {
+ requestWritePermission.launch()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putBoolean("mFullscreen", mFullscreen)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ ActivityFeatures.Builder()
+ .begin()
+ .setHideNavigationMenu(true)
+ .setBarsColored(colored = false, invertIcons = false)
+ .build()
+ .apply(this)
+ }
+
+ internal fun toggleFullscreen() {
+ mFullscreen = !mFullscreen
+ resolveFullscreenViews()
+ }
+
+ private fun resolveFullscreenViews() {
+ mToolbar?.visibility = if (mFullscreen) View.GONE else View.VISIBLE
+ mDownload?.visibility = if (mFullscreen) View.GONE else View.VISIBLE
+ mShare?.visibility = if (mFullscreen) View.GONE else View.VISIBLE
+ mLink?.visibility = if (mFullscreen || !hasExternalUrl) View.GONE else View.VISIBLE
+ }
+
+ override fun getPresenterFactory(saveInstanceState: Bundle?): IPresenterFactory =
+ object : IPresenterFactory {
+ override fun create(): StoryPagerPresenter {
+ val aid = requireArguments().getInt(Extra.ACCOUNT_ID)
+ val index = requireArguments().getInt(Extra.INDEX)
+ val stories: ArrayList = if (FenrirNative.isNativeLoaded && Settings.get()
+ .other().isNative_parcel_story
+ ) ParcelNative.loadParcelableArrayList(
+ requireArguments().getLong(
+ Extra.STORY
+ ), Story.NativeCreator, ParcelFlags.EMPTY_LIST
+ )!! else
+ requireArguments().getParcelableArrayListCompat(Extra.STORY)!!
+ if (FenrirNative.isNativeLoaded && Settings.get()
+ .other().isNative_parcel_story
+ ) {
+ requireArguments().putLong(Extra.STORY, 0)
+ }
+ return StoryPagerPresenter(
+ aid,
+ stories,
+ index,
+ this@StoryPagerActivity,
+ saveInstanceState
+ )
+ }
+ }
+
+ override fun displayData(pageCount: Int, selectedIndex: Int) {
+ val adapter = Adapter(pageCount)
+ mViewPager?.adapter = adapter
+ mViewPager?.setCurrentItem(selectedIndex, false)
+ }
+
+ override fun setAspectRatioAt(position: Int, w: Int, h: Int) {
+ findByPosition(position)?.setAspectRatio(w, h)
+ }
+
+ override fun setPreparingProgressVisible(position: Int, preparing: Boolean) {
+ for (i in 0 until mHolderSparseArray.size()) {
+ val key = mHolderSparseArray.keyAt(i)
+ val holder = findByPosition(key)
+ val isCurrent = position == key
+ val progressVisible = isCurrent && preparing
+ holder?.setProgressVisible(progressVisible)
+ holder?.setSurfaceVisible(if (isCurrent && !preparing) View.VISIBLE else View.GONE)
+ }
+ }
+
+ override fun attachDisplayToPlayer(adapterPosition: Int, storyPlayer: IStoryPlayer?) {
+ val holder = findByPosition(adapterPosition)
+ if (holder?.isSurfaceReady == true) {
+ storyPlayer?.setDisplay(holder.mSurfaceHolder)
+ }
+ }
+
+ override fun setToolbarTitle(@StringRes titleRes: Int, vararg params: Any?) {
+ supportActionBar?.title = getString(titleRes, *params)
+ }
+
+ override fun setToolbarSubtitle(story: Story, account_id: Int) {
+ supportActionBar?.subtitle = story.owner?.fullName
+ mAvatar?.setOnClickListener {
+ story.owner?.let { it1 ->
+ PlaceFactory.getOwnerWallPlace(account_id, it1)
+ .tryOpenWith(this)
+ }
+ }
+ mAvatar?.let {
+ ViewUtils.displayAvatar(
+ it,
+ transformation,
+ story.owner?.maxSquareAvatar,
+ Constants.PICASSO_TAG
+ )
+ }
+ if (story.expires <= 0) mExp?.visibility = View.GONE else {
+ mExp?.visibility = View.VISIBLE
+ val exp = (story.expires - Calendar.getInstance().time.time / 1000) / 3600
+ mExp?.text = getString(
+ R.string.expires,
+ exp.toString(),
+ getString(
+ Utils.declOfNum(
+ exp,
+ intArrayOf(R.string.hour, R.string.hour_sec, R.string.hours)
+ )
+ )
+ )
+ }
+ if (story.target_url.isNullOrEmpty()) {
+ mLink?.visibility = View.GONE
+ hasExternalUrl = false
+ } else {
+ hasExternalUrl = true
+ mLink?.visibility = View.VISIBLE
+ mLink?.setOnClickListener {
+ LinkHelper.openUrl(
+ this,
+ account_id,
+ story.target_url, false
+ )
+ }
+ }
+ }
+
+ override fun onShare(story: Story, account_id: Int) {
+ SendAttachmentsActivity.startForSendAttachments(this, account_id, story)
+ }
+
+ override fun configHolder(
+ adapterPosition: Int,
+ progress: Boolean,
+ aspectRatioW: Int,
+ aspectRatioH: Int
+ ) {
+ val holder = findByPosition(adapterPosition)
+ holder?.setProgressVisible(progress)
+ holder?.setAspectRatio(aspectRatioW, aspectRatioH)
+ holder?.setSurfaceVisible(if (progress) View.GONE else View.VISIBLE)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ helpDisposable.dispose()
+ }
+
+ override fun onNext() {
+ mViewPager?.let {
+ it.adapter?.let { so ->
+ if (so.itemCount > it.currentItem + 1) {
+ it.setCurrentItem(it.currentItem + 1, true)
+ }
+ }
+ }
+ }
+
+ internal fun fireHolderCreate(holder: MultiHolder) {
+ presenter?.fireHolderCreate(holder.bindingAdapterPosition)
+ }
+
+ private fun findByPosition(position: Int): MultiHolder? {
+ val weak = mHolderSparseArray[position]
+ return weak?.get()
+ }
+
+ open class MultiHolder internal constructor(rootView: View) :
+ RecyclerView.ViewHolder(rootView) {
+ lateinit var mSurfaceHolder: SurfaceHolder
+ open val isSurfaceReady: Boolean
+ get() = false
+
+ open fun setProgressVisible(visible: Boolean) {}
+ open fun setAspectRatio(w: Int, h: Int) {}
+ open fun setSurfaceVisible(Vis: Int) {}
+ open fun bindTo(story: Story) {}
+ }
+
+ private inner class Holder(rootView: View) : MultiHolder(rootView), SurfaceHolder.Callback {
+ val mSurfaceView: ExpandableSurfaceView = rootView.findViewById(R.id.videoSurface)
+ val mProgressBar: RLottieImageView
+ override var isSurfaceReady = false
+ override fun surfaceCreated(holder: SurfaceHolder) {
+ isSurfaceReady = true
+ presenter?.fireSurfaceCreated(bindingAdapterPosition)
+ }
+
+ override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
+ override fun surfaceDestroyed(holder: SurfaceHolder) {
+ isSurfaceReady = false
+ }
+
+ override fun setProgressVisible(visible: Boolean) {
+ mProgressBar.visibility = if (visible) View.VISIBLE else View.GONE
+ if (visible) {
+ mProgressBar.fromRes(
+ dev.ragnarok.fenrir_common.R.raw.loading,
+ Utils.dp(100F),
+ Utils.dp(100F),
+ intArrayOf(
+ 0x000000,
+ CurrentTheme.getColorPrimary(this@StoryPagerActivity),
+ 0x777777,
+ CurrentTheme.getColorSecondary(this@StoryPagerActivity)
+ )
+ )
+ mProgressBar.playAnimation()
+ } else {
+ mProgressBar.clearAnimationDrawable()
+ }
+ }
+
+ override fun setAspectRatio(w: Int, h: Int) {
+ mSurfaceView.setAspectRatio(w, h)
+ }
+
+ override fun setSurfaceVisible(Vis: Int) {
+ mSurfaceView.visibility = Vis
+ }
+
+ init {
+ mSurfaceHolder = mSurfaceView.holder
+ mSurfaceHolder.addCallback(this)
+ mProgressBar = rootView.findViewById(R.id.preparing_progress_bar)
+ mSurfaceView.setOnClickListener { toggleFullscreen() }
+ }
+ }
+
+ private inner class PhotoViewHolder(view: View) : MultiHolder(view), Callback {
+ val reload: FloatingActionButton
+ private val mPicassoLoadCallback: WeakPicassoLoadCallback
+ val photo: TouchImageView
+ val progress: RLottieImageView
+ var animationDispose: Disposable = Disposable.disposed()
+ private var mAnimationLoaded = false
+ private var mLoadingNow = false
+ override fun bindTo(story: Story) {
+ photo.resetZoom()
+ if (story.isIs_expired) {
+ createCustomToast(this@StoryPagerActivity).showToastError(R.string.is_expired)
+ mLoadingNow = false
+ resolveProgressVisibility(true)
+ return
+ }
+ if (story.photo == null) return
+ val url = story.photo?.getUrlForSize(PhotoSize.W, true)
+ reload.setOnClickListener {
+ reload.visibility = View.INVISIBLE
+ if (url.nonNullNoEmpty()) {
+ loadImage(url)
+ } else PicassoInstance.with().cancelRequest(photo)
+ }
+ if (url.nonNullNoEmpty()) {
+ loadImage(url)
+ } else {
+ PicassoInstance.with().cancelRequest(photo)
+ createCustomToast(this@StoryPagerActivity).showToast(R.string.empty_url)
+ }
+ }
+
+ private fun resolveProgressVisibility(forceStop: Boolean) {
+ animationDispose.dispose()
+ if (mAnimationLoaded && !mLoadingNow && !forceStop) {
+ mAnimationLoaded = false
+ val k = ObjectAnimator.ofFloat(progress, View.ALPHA, 0.0f).setDuration(1000)
+ k.addListener(object : StubAnimatorListener() {
+ override fun onAnimationEnd(animation: Animator) {
+ progress.clearAnimationDrawable()
+ progress.visibility = View.GONE
+ progress.alpha = 1f
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ progress.clearAnimationDrawable()
+ progress.visibility = View.GONE
+ progress.alpha = 1f
+ }
+ })
+ k.start()
+ } else if (mAnimationLoaded && !mLoadingNow) {
+ mAnimationLoaded = false
+ progress.clearAnimationDrawable()
+ progress.visibility = View.GONE
+ } else if (mLoadingNow) {
+ animationDispose = Completable.create {
+ it.onComplete()
+ }.delay(300, TimeUnit.MILLISECONDS).fromIOToMain().subscribe({
+ mAnimationLoaded = true
+ progress.visibility = View.VISIBLE
+ progress.fromRes(
+ dev.ragnarok.fenrir_common.R.raw.loading,
+ Utils.dp(100F),
+ Utils.dp(100F),
+ intArrayOf(
+ 0x000000,
+ CurrentTheme.getColorPrimary(this@StoryPagerActivity),
+ 0x777777,
+ CurrentTheme.getColorSecondary(this@StoryPagerActivity)
+ )
+ )
+ progress.playAnimation()
+ }, RxUtils.ignore())
+ }
+ }
+
+ private fun loadImage(url: String) {
+ mLoadingNow = true
+ resolveProgressVisibility(true)
+ PicassoInstance.with()
+ .load(url)
+ .into(photo, mPicassoLoadCallback)
+ }
+
+ @IdRes
+ private fun idOfImageView(): Int {
+ return R.id.image_view
+ }
+
+ @IdRes
+ private fun idOfProgressBar(): Int {
+ return R.id.progress_bar
+ }
+
+ override fun onSuccess() {
+ mLoadingNow = false
+ resolveProgressVisibility(false)
+ reload.visibility = View.INVISIBLE
+ }
+
+ override fun onError(t: Throwable) {
+ mLoadingNow = false
+ resolveProgressVisibility(true)
+ reload.visibility = View.VISIBLE
+ }
+
+ init {
+ photo = view.findViewById(idOfImageView())
+ photo.maxZoom = 8f
+ photo.doubleTapScale = 2f
+ photo.doubleTapMaxZoom = 4f
+ progress = view.findViewById(idOfProgressBar())
+ reload = view.findViewById(R.id.goto_button)
+ mPicassoLoadCallback = WeakPicassoLoadCallback(this)
+ photo.setOnClickListener { toggleFullscreen() }
+ }
+ }
+
+ private inner class Adapter(val mPageCount: Int) : RecyclerView.Adapter() {
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onCreateViewHolder(container: ViewGroup, viewType: Int): MultiHolder {
+ if (viewType == 0) return Holder(
+ LayoutInflater.from(container.context)
+ .inflate(R.layout.content_story_page, container, false)
+ )
+ val ret = PhotoViewHolder(
+ LayoutInflater.from(container.context)
+ .inflate(R.layout.content_photo_page, container, false)
+ )
+ ret.photo.setOnTouchListener { view: View, event: MotionEvent ->
+ if (event.pointerCount >= 2 || view.canScrollVertically(1) && view.canScrollVertically(
+ -1
+ )
+ ) {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
+ container.requestDisallowInterceptTouchEvent(true)
+ return@setOnTouchListener false
+ }
+ MotionEvent.ACTION_UP -> {
+ container.requestDisallowInterceptTouchEvent(false)
+ return@setOnTouchListener true
+ }
+ }
+ }
+ true
+ }
+ return ret
+ }
+
+ override fun onBindViewHolder(holder: MultiHolder, position: Int) {
+ presenter?.let {
+ if (!it.isStoryIsVideo(position)) holder.bindTo(it.getStory(position))
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return if (presenter?.isStoryIsVideo(position) == true) 0 else 1
+ }
+
+ override fun getItemCount(): Int {
+ return mPageCount
+ }
+
+ override fun onViewDetachedFromWindow(holder: MultiHolder) {
+ super.onViewDetachedFromWindow(holder)
+ mHolderSparseArray.remove(holder.bindingAdapterPosition)
+ }
+
+ override fun onViewAttachedToWindow(holder: MultiHolder) {
+ super.onViewAttachedToWindow(holder)
+ mHolderSparseArray.put(holder.bindingAdapterPosition, WeakReference(holder))
+ fireHolderCreate(holder)
+ }
+
+ init {
+ mHolderSparseArray.clear()
+ }
+ }
+
+ companion object {
+ const val ACTION_OPEN =
+ "dev.ragnarok.fenrir.activity.storypager.StoryPagerActivity"
+
+ fun newInstance(context: Context, args: Bundle?): Intent {
+ val ph = Intent(context, StoryPagerActivity::class.java)
+ val targetArgs = Bundle()
+ targetArgs.putAll(args)
+ ph.action = ACTION_OPEN
+ ph.putExtras(targetArgs)
+ return ph
+ }
+
+ fun buildArgs(aid: Int, stories: ArrayList, index: Int): Bundle {
+ val args = Bundle()
+ args.putInt(Extra.ACCOUNT_ID, aid)
+ args.putInt(Extra.INDEX, index)
+ if (FenrirNative.isNativeLoaded && Settings.get().other().isNative_parcel_story) {
+ args.putLong(
+ Extra.STORY,
+ ParcelNative.createParcelableList(stories, ParcelFlags.NULL_LIST)
+ )
+ } else {
+ args.putParcelableArrayList(Extra.STORY, stories)
+ }
+ return args
+ }
+ }
+}
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/storypager/StoryPagerPresenter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/storypager/StoryPagerPresenter.kt
new file mode 100644
index 000000000..56ca6bb57
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/activity/storypager/StoryPagerPresenter.kt
@@ -0,0 +1,326 @@
+package dev.ragnarok.fenrir.activity.storypager
+
+import android.content.Context
+import android.os.Bundle
+import dev.ragnarok.fenrir.App.Companion.instance
+import dev.ragnarok.fenrir.Includes.storyPlayerFactory
+import dev.ragnarok.fenrir.R
+import dev.ragnarok.fenrir.fragment.base.AccountDependencyPresenter
+import dev.ragnarok.fenrir.media.story.IStoryPlayer
+import dev.ragnarok.fenrir.media.story.IStoryPlayer.IStatusChangeListener
+import dev.ragnarok.fenrir.model.Photo
+import dev.ragnarok.fenrir.model.PhotoSize
+import dev.ragnarok.fenrir.model.Story
+import dev.ragnarok.fenrir.model.VideoSize
+import dev.ragnarok.fenrir.nonNullNoEmpty
+import dev.ragnarok.fenrir.requireNonNull
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.util.AppPerms.hasReadWriteStoragePermission
+import dev.ragnarok.fenrir.util.DownloadWorkUtils.doDownloadPhoto
+import dev.ragnarok.fenrir.util.DownloadWorkUtils.doDownloadVideo
+import dev.ragnarok.fenrir.util.DownloadWorkUtils.makeLegalFilename
+import dev.ragnarok.fenrir.util.Utils.firstNonEmptyString
+import java.io.File
+import java.util.*
+import kotlin.math.abs
+
+class StoryPagerPresenter(
+ accountId: Int,
+ private val mStories: ArrayList,
+ private var mCurrentIndex: Int,
+ private val context: Context,
+ savedInstanceState: Bundle?
+) : AccountDependencyPresenter(accountId, savedInstanceState),
+ IStatusChangeListener, IStoryPlayer.IVideoSizeChangeListener {
+ private var mStoryPlayer: IStoryPlayer? = null
+ fun isStoryIsVideo(pos: Int): Boolean {
+ return mStories[pos].photo == null && mStories[pos].video != null
+ }
+
+ fun getStory(pos: Int): Story {
+ return mStories[pos]
+ }
+
+ override fun onGuiCreated(viewHost: IStoryPagerView) {
+ super.onGuiCreated(viewHost)
+ viewHost.displayData(mStories.size, mCurrentIndex)
+ resolveToolbarTitle()
+ resolvePlayerDisplay()
+ resolveAspectRatio()
+ resolvePreparingProgress()
+ resolveToolbarSubtitle()
+ }
+
+ fun fireSurfaceCreated(adapterPosition: Int) {
+ if (mCurrentIndex == adapterPosition) {
+ resolvePlayerDisplay()
+ }
+ }
+
+ private fun resolveToolbarTitle() {
+ view?.setToolbarTitle(
+ R.string.image_number,
+ mCurrentIndex + 1,
+ mStories.size
+ )
+ }
+
+ private fun resolvePlayerDisplay() {
+ if (guiIsReady) {
+ view?.attachDisplayToPlayer(
+ mCurrentIndex,
+ mStoryPlayer
+ )
+ } else {
+ mStoryPlayer?.setDisplay(null)
+ }
+ }
+
+ private fun initStoryPlayer() {
+ if (mStoryPlayer != null) {
+ val old: IStoryPlayer? = mStoryPlayer
+ mStoryPlayer = null
+ old?.release()
+ }
+ val story = mStories[mCurrentIndex]
+ if (story.video == null) {
+ return
+ }
+ val url = firstNonEmptyString(
+ story.video?.mp4link2160, story.video?.mp4link1440,
+ story.video?.mp4link1080, story.video?.mp4link720, story.video?.mp4link480,
+ story.video?.mp4link360, story.video?.mp4link240
+ )
+ if (url == null) {
+ view?.showError(R.string.unable_to_play_file)
+ return
+ }
+ mStoryPlayer = storyPlayerFactory.createStoryPlayer(url, false)
+ mStoryPlayer?.addStatusChangeListener(this)
+ mStoryPlayer?.addVideoSizeChangeListener(this)
+ try {
+ mStoryPlayer?.play()
+ } catch (e: Exception) {
+ view?.showError(R.string.unable_to_play_file)
+ }
+ }
+
+ private fun selectPage(position: Int) {
+ if (mCurrentIndex == position) {
+ return
+ }
+ mCurrentIndex = position
+ initStoryPlayer()
+ }
+
+ private val isMy: Boolean
+ get() = mStories[mCurrentIndex].ownerId == accountId
+
+ private fun resolveAspectRatio() {
+ if (mStoryPlayer == null) {
+ return
+ }
+ val size = mStoryPlayer?.videoSize
+ if (size != null) {
+ view?.setAspectRatioAt(
+ mCurrentIndex,
+ size.width.coerceAtLeast(1),
+ size.height.coerceAtLeast(1)
+ )
+ } else {
+ view?.setAspectRatioAt(
+ mCurrentIndex,
+ 1,
+ 1
+ )
+ }
+ }
+
+ private fun resolvePreparingProgress() {
+ val preparing =
+ mStoryPlayer != null && mStoryPlayer?.playerStatus == IStoryPlayer.IStatus.PREPARING
+ view?.setPreparingProgressVisible(
+ mCurrentIndex,
+ preparing
+ )
+ }
+
+ private fun resolveToolbarSubtitle() {
+ view?.setToolbarSubtitle(
+ mStories[mCurrentIndex],
+ accountId
+ )
+ }
+
+ fun firePageSelected(position: Int) {
+ if (mCurrentIndex == position) {
+ return
+ }
+ selectPage(position)
+ resolveToolbarTitle()
+ resolveToolbarSubtitle()
+ resolvePreparingProgress()
+ }
+
+ fun fireHolderCreate(adapterPosition: Int) {
+ if (!isStoryIsVideo(adapterPosition)) return
+ val isProgress =
+ adapterPosition == mCurrentIndex && (mStoryPlayer == null || mStoryPlayer?.playerStatus == IStoryPlayer.IStatus.PREPARING)
+ var size = if (mStoryPlayer == null) null else mStoryPlayer?.videoSize
+ if (size == null) {
+ size = DEF_SIZE
+ }
+ if (size.width <= 0) {
+ size.setWidth(1)
+ }
+ if (size.height <= 0) {
+ size.setHeight(1)
+ }
+ view?.configHolder(
+ adapterPosition,
+ isProgress,
+ size.width,
+ size.width
+ )
+ }
+
+ fun fireShareButtonClick() {
+ val story = mStories[mCurrentIndex]
+ view?.onShare(story, accountId)
+ }
+
+ fun fireDownloadButtonClick() {
+ if (!hasReadWriteStoragePermission(instance)) {
+ view?.requestWriteExternalStoragePermission()
+ return
+ }
+ downloadImpl()
+ }
+
+ private fun onWritePermissionResolved() {
+ if (hasReadWriteStoragePermission(instance)) {
+ downloadImpl()
+ }
+ }
+
+ fun fireWritePermissionResolved() {
+ onWritePermissionResolved()
+ }
+
+ public override fun onGuiPaused() {
+ super.onGuiPaused()
+ mStoryPlayer?.pause()
+ }
+
+ public override fun onGuiResumed() {
+ super.onGuiResumed()
+ if (mStoryPlayer != null) {
+ try {
+ mStoryPlayer?.play()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ override fun onDestroyed() {
+ if (mStoryPlayer != null) {
+ mStoryPlayer?.release()
+ }
+ super.onDestroyed()
+ }
+
+ private fun downloadImpl() {
+ val story = mStories[mCurrentIndex]
+ if (story.photo != null) doSaveOnDrive(story)
+ if (story.video != null) {
+ val url = firstNonEmptyString(
+ story.video?.mp4link2160, story.video?.mp4link1440,
+ story.video?.mp4link1080, story.video?.mp4link720, story.video?.mp4link480,
+ story.video?.mp4link360, story.video?.mp4link240
+ )
+ story.video?.setTitle(story.owner?.fullName)
+ url.nonNullNoEmpty {
+ story.video.requireNonNull { s ->
+ doDownloadVideo(context, s, it, "Story")
+ }
+ }
+ }
+ }
+
+ private fun doSaveOnDrive(photo: Story) {
+ val dir = File(Settings.get().other().photoDir)
+ if (!dir.isDirectory) {
+ val created = dir.mkdirs()
+ if (!created) {
+ view?.showError("Can't create directory $dir")
+ return
+ }
+ } else dir.setLastModified(Calendar.getInstance().time.time)
+ photo.photo?.let {
+ downloadResult(photo.owner?.fullName?.let { it1 ->
+ makeLegalFilename(
+ it1,
+ null
+ )
+ }, dir, it)
+ }
+ }
+
+ private fun transform_owner(owner_id: Int): String {
+ return if (owner_id < 0) "club" + abs(owner_id) else "id$owner_id"
+ }
+
+ private fun downloadResult(Prefix: String?, dirF: File, photo: Photo) {
+ var dir = dirF
+ if (Prefix != null && Settings.get().other().isPhoto_to_user_dir) {
+ val dir_final = File(dir.absolutePath + "/" + Prefix)
+ if (!dir_final.isDirectory) {
+ val created = dir_final.mkdirs()
+ if (!created) {
+ view?.showError("Can't create directory $dir_final")
+ return
+ }
+ } else dir_final.setLastModified(Calendar.getInstance().time.time)
+ dir = dir_final
+ }
+ val url = photo.getUrlForSize(PhotoSize.W, true)
+ if (url != null) {
+ doDownloadPhoto(
+ context,
+ url,
+ dir.absolutePath,
+ (if (Prefix != null) Prefix + "_" else "") + transform_owner(photo.ownerId) + "_" + photo.getObjectId()
+ )
+ }
+ }
+
+ override fun onPlayerStatusChange(
+ player: IStoryPlayer,
+ previousStatus: Int,
+ currentStatus: Int
+ ) {
+ if (mStoryPlayer === player) {
+ if (currentStatus == IStoryPlayer.IStatus.ENDED) {
+ view?.onNext()
+ return
+ }
+ resolvePreparingProgress()
+ resolvePlayerDisplay()
+ }
+ }
+
+ override fun onVideoSizeChanged(player: IStoryPlayer, size: VideoSize) {
+ if (mStoryPlayer === player) {
+ resolveAspectRatio()
+ }
+ }
+
+ companion object {
+ private val DEF_SIZE = VideoSize(1, 1)
+ }
+
+ init {
+ initStoryPlayer()
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AbsVkApiInterceptor.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AbsVkApiInterceptor.kt
new file mode 100644
index 000000000..4b294772d
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AbsVkApiInterceptor.kt
@@ -0,0 +1,84 @@
+package dev.ragnarok.fenrir.api
+
+import dev.ragnarok.fenrir.AccountType
+import dev.ragnarok.fenrir.Constants
+import dev.ragnarok.fenrir.Includes.provideApplicationContext
+import dev.ragnarok.fenrir.exception.UnauthorizedException
+import dev.ragnarok.fenrir.nonNullNoEmpty
+import dev.ragnarok.fenrir.util.Utils
+import okhttp3.FormBody
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
+import java.io.IOException
+
+abstract class AbsVkApiInterceptor(private val version: String) :
+ Interceptor {
+ protected abstract val token: String?
+
+ @AccountType
+ abstract val type: Int
+ protected abstract val accountId: Int
+
+ /*
+ private String RECEIPT_GMS_TOKEN() {
+ try {
+ GoogleApiAvailability instance = GoogleApiAvailability.getInstance();
+ int isGooglePlayServicesAvailable = instance.isGooglePlayServicesAvailable(Includes.provideApplicationContext());
+ if (isGooglePlayServicesAvailable != 0) {
+ return null;
+ }
+ return FirebaseInstanceId.getInstance().getToken("54740537194", "id" + getAccountId());
+ } catch (Throwable th) {
+ th.printStackTrace();
+ return null;
+ }
+ }
+ */
+
+ @Throws(IOException::class)
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val vkAccessToken: String = token.nonNullNoEmpty({ it },
+ { throw UnauthorizedException("No authorization! Please, login and retry") })
+ val original: Request = chain.request()
+
+ val formBuilder = FormBody.Builder()
+ val body = original.body
+ var hasVersion = false
+ var hasDeviceId = false
+ var hasAccessToken = false
+ if (body is FormBody) {
+ for (i in 0 until body.size) {
+ val name = body.name(i)
+ when (name) {
+ "v" -> {
+ hasVersion = true
+ }
+ "device_id" -> hasDeviceId = true
+ "access_token" -> hasAccessToken = true
+ }
+ val value = body.value(i)
+ formBuilder.add(name, value)
+ }
+ }
+ if (!hasVersion) {
+ formBuilder.add("v", version)
+ }
+ if (!hasAccessToken) {
+ formBuilder.add("access_token", vkAccessToken)
+ }
+ formBuilder.add("lang", Constants.DEVICE_COUNTRY_CODE)
+ .add("https", "1")
+ if (!hasDeviceId) {
+ formBuilder.add(
+ "device_id",
+ Utils.getDeviceId(provideApplicationContext())
+ )
+ }
+ return chain.proceed(
+ original.newBuilder()
+ .method("POST", formBuilder.build())
+ .build()
+ )
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/ApiException.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/ApiException.kt
new file mode 100644
index 000000000..8706b7585
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/ApiException.kt
@@ -0,0 +1,11 @@
+package dev.ragnarok.fenrir.api
+
+import dev.ragnarok.fenrir.api.model.Error
+import dev.ragnarok.fenrir.util.Utils
+
+class ApiException(val error: Error) : Exception(
+ Utils.firstNonEmptyString(
+ error["method"],
+ " "
+ ) + ": " + Utils.firstNonEmptyString(error.errorMsg, " ")
+)
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/Apis.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/Apis.kt
new file mode 100644
index 000000000..5b6a52631
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/Apis.kt
@@ -0,0 +1,11 @@
+package dev.ragnarok.fenrir.api
+
+import dev.ragnarok.fenrir.Includes.networkInterfaces
+import dev.ragnarok.fenrir.api.interfaces.INetworker
+
+object Apis {
+
+ fun get(): INetworker {
+ return networkInterfaces
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/Auth.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/Auth.kt
new file mode 100644
index 000000000..21cd6d0b2
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/Auth.kt
@@ -0,0 +1,65 @@
+package dev.ragnarok.fenrir.api
+
+import android.util.Log
+import dev.ragnarok.fenrir.AccountType
+import dev.ragnarok.fenrir.Constants
+import dev.ragnarok.fenrir.Includes.provideApplicationContext
+import dev.ragnarok.fenrir.api.util.VKStringUtils.extractPattern
+import dev.ragnarok.fenrir.nonNullNoEmpty
+import dev.ragnarok.fenrir.util.Utils
+import java.io.UnsupportedEncodingException
+import java.net.URLEncoder
+
+object Auth {
+ const val redirect_url = "https://oauth.vk.com/blank.html"
+ private const val TAG = "Fenrir.Auth"
+
+ @Throws(UnsupportedEncodingException::class)
+ fun getUrl(api_id: String, scope: String, groupIds: String): String {
+ var url = "https://oauth.vk.com/authorize?client_id=$api_id"
+ url = (url + "&display=mobile&scope="
+ + scope + "&redirect_uri=" + URLEncoder.encode(
+ redirect_url,
+ "utf-8"
+ ) + "&response_type=token"
+ + "&v=" + URLEncoder.encode(
+ Constants.API_VERSION,
+ "utf-8"
+ ) + "&lang=" + URLEncoder.encode(
+ Constants.DEVICE_COUNTRY_CODE, "utf-8"
+ ) + "&device_id=" + URLEncoder.encode(
+ Utils.getDeviceId(
+ provideApplicationContext()
+ ), "utf-8"
+ ))
+ if (groupIds.nonNullNoEmpty()) {
+ url = "$url&group_ids=$groupIds"
+ }
+ return url
+ }
+
+ //http://vk.com/dev/permission
+ //return "notify,friends,photos,audio,video,stories,pages,status,notes,messages,wall,offline,docs,groups,notifications,stats,email,market";
+
+ val scope: String
+ get() =//http://vk.com/dev/permission
+ //return "notify,friends,photos,audio,video,stories,pages,status,notes,messages,wall,offline,docs,groups,notifications,stats,email,market";
+ if (Constants.DEFAULT_ACCOUNT_TYPE == AccountType.KATE) {
+ "notify,friends,photos,audio,video,docs,status,notes,pages,wall,groups,messages,offline,notifications,stories"
+ } else {
+ "all"
+ }
+
+ @Throws(Exception::class)
+ fun parseRedirectUrl(url: String): Array {
+ //url is something like http://api.vkontakte.ru/blank.html#access_token=66e8f7a266af0dd477fcd3916366b17436e66af77ac352aeb270be99df7deeb&expires_in=0&user_id=7657164
+ val access_token = extractPattern(url, "access_token=(.*?)&")
+ Log.i(TAG, "access_token=$access_token")
+ val user_id = extractPattern(url, "user_id=(\\d*)")
+ Log.i(TAG, "user_id=$user_id")
+ if (user_id == null || user_id.isEmpty() || access_token == null || access_token.isEmpty()) {
+ throw Exception("Failed to parse redirect url $url")
+ }
+ return arrayOf(access_token, user_id)
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AuthException.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AuthException.kt
new file mode 100644
index 000000000..f0b74c4e9
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AuthException.kt
@@ -0,0 +1,11 @@
+package dev.ragnarok.fenrir.api
+
+class AuthException(val code: String, message: String?) : Exception(message) {
+ override val message: String
+ get() {
+ val desc = super.message
+ return if (desc != null && desc.isNotEmpty()) {
+ desc
+ } else "Unexpected auth error, code: [$code]"
+ }
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CaptchaNeedException.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CaptchaNeedException.kt
new file mode 100644
index 000000000..f0a789cf7
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CaptchaNeedException.kt
@@ -0,0 +1,3 @@
+package dev.ragnarok.fenrir.api
+
+class CaptchaNeedException(val sid: String?, val img: String?) : Exception()
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CaptchaProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CaptchaProvider.kt
new file mode 100644
index 000000000..4b254a4da
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CaptchaProvider.kt
@@ -0,0 +1,88 @@
+package dev.ragnarok.fenrir.api
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import dev.ragnarok.fenrir.activity.CaptchaActivity.Companion.createIntent
+import dev.ragnarok.fenrir.api.model.Captcha
+import io.reactivex.rxjava3.core.Completable
+import io.reactivex.rxjava3.core.Observable
+import io.reactivex.rxjava3.core.Scheduler
+import io.reactivex.rxjava3.subjects.PublishSubject
+import java.util.*
+
+class CaptchaProvider(private val app: Context, private val uiScheduler: Scheduler) :
+ ICaptchaProvider {
+ private val entryMap: MutableMap = Collections.synchronizedMap(HashMap())
+ private val cancelingNotifier: PublishSubject = PublishSubject.create()
+ private val waitingNotifier: PublishSubject = PublishSubject.create()
+ override fun requestCaptha(sid: String?, captcha: Captcha) {
+ sid ?: return
+ entryMap[sid] = Entry()
+ startCapthaActivity(app, sid, captcha)
+ }
+
+ @SuppressLint("CheckResult")
+ private fun startCapthaActivity(context: Context, sid: String, captcha: Captcha) {
+ Completable.complete()
+ .observeOn(uiScheduler)
+ .subscribe {
+ val intent = createIntent(context, sid, captcha.img)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ }
+ }
+
+ override fun cancel(sid: String) {
+ entryMap.remove(sid)
+ cancelingNotifier.onNext(sid)
+ }
+
+ override fun observeCanceling(): Observable {
+ return cancelingNotifier
+ }
+
+ @Throws(OutOfDateException::class)
+ override fun lookupCode(sid: String): String? {
+ val iterator: MutableIterator> = entryMap.entries.iterator()
+ while (iterator.hasNext()) {
+ val (lookupsid, lookupEntry) = iterator.next()
+ if (System.currentTimeMillis() - lookupEntry.lastActivityTime > MAX_WAIT_DELAY) {
+ iterator.remove()
+ } else {
+ waitingNotifier.onNext(lookupsid)
+ }
+ }
+ val entry = entryMap[sid] ?: throw OutOfDateException()
+ return entry.code
+ }
+
+ override fun observeWaiting(): Observable {
+ return waitingNotifier
+ }
+
+ override fun notifyThatCaptchaEntryActive(sid: String) {
+ val entry = entryMap[sid]
+ if (entry != null) {
+ entry.lastActivityTime = System.currentTimeMillis()
+ }
+ }
+
+ override fun enterCode(sid: String, code: String?) {
+ val entry = entryMap[sid]
+ if (entry != null) {
+ entry.code = code
+ }
+ }
+
+ private class Entry {
+ var code: String? = null
+ var lastActivityTime: Long = System.currentTimeMillis()
+
+ }
+
+ companion object {
+ private const val MAX_WAIT_DELAY = 15 * 60 * 1000
+ }
+
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CustomTokenVkApiInterceptor.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CustomTokenVkApiInterceptor.kt
new file mode 100644
index 000000000..fa18bee37
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CustomTokenVkApiInterceptor.kt
@@ -0,0 +1,28 @@
+package dev.ragnarok.fenrir.api
+
+import dev.ragnarok.fenrir.AccountType
+import dev.ragnarok.fenrir.Constants
+import dev.ragnarok.fenrir.settings.Settings
+
+internal class CustomTokenVkApiInterceptor(
+ override val token: String?,
+ v: String,
+ @AccountType private val accountType: Int,
+ private val account_id: Int?
+) : AbsVkApiInterceptor(
+ v
+) {
+
+ @AccountType
+ override val type: Int
+ get() {
+ if (accountType == AccountType.BY_TYPE && account_id == null) {
+ return Constants.DEFAULT_ACCOUNT_TYPE
+ } else if (accountType == AccountType.BY_TYPE) {
+ return Settings.get().accounts().getType(account_id ?: -1)
+ }
+ return accountType
+ }
+ override val accountId: Int
+ get() = account_id ?: -1
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/DefaultVkApiInterceptor.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/DefaultVkApiInterceptor.kt
new file mode 100644
index 000000000..4371a1f3c
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/DefaultVkApiInterceptor.kt
@@ -0,0 +1,22 @@
+package dev.ragnarok.fenrir.api
+
+import dev.ragnarok.fenrir.AccountType
+import dev.ragnarok.fenrir.settings.Settings
+
+class DefaultVkApiInterceptor internal constructor(
+ override val accountId: Int,
+ v: String
+) : AbsVkApiInterceptor(
+ v
+) {
+ override val token: String?
+ get() = Settings.get()
+ .accounts()
+ .getAccessToken(accountId)
+
+ @AccountType
+ override val type: Int
+ get() = Settings.get()
+ .accounts()
+ .getType(accountId)
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/HttpLoggerAndParser.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/HttpLoggerAndParser.kt
new file mode 100644
index 000000000..12da58264
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/HttpLoggerAndParser.kt
@@ -0,0 +1,158 @@
+package dev.ragnarok.fenrir.api
+
+import android.annotation.SuppressLint
+import dev.ragnarok.fenrir.Constants
+import dev.ragnarok.fenrir.api.model.Params
+import dev.ragnarok.fenrir.model.ParserType
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.util.OkHttp3LoggingInterceptor
+import dev.ragnarok.fenrir.util.Utils
+import okhttp3.*
+import okio.*
+import java.io.IOException
+import java.security.SecureRandom
+import java.security.cert.CertificateException
+import java.security.cert.X509Certificate
+import javax.net.ssl.SSLContext
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.TrustManager
+import javax.net.ssl.X509TrustManager
+
+object HttpLoggerAndParser {
+ /*
+ fun selectConverterFactory(
+ json: Converter.Factory,
+ msgpack: Converter.Factory
+ ): Converter.Factory {
+ return if (Settings.get().other().currentParser == ParserType.MSGPACK) {
+ msgpack
+ } else {
+ json
+ }
+ }
+ */
+
+ abstract class GzipFormBody(val original: List) : RequestBody()
+
+ fun FormBody.gzipFormBody(): GzipFormBody {
+ val o = ArrayList(size)
+ for (i in 0 until size) {
+ val tmp = Params()
+ tmp.key = name(i)
+ tmp.value = value(i)
+ o.add(tmp)
+ }
+ return object : GzipFormBody(o) {
+ override fun contentType(): MediaType {
+ return this@gzipFormBody.contentType()
+ }
+
+ override fun contentLength(): Long {
+ val g = Buffer()
+ val f = GzipSink(g).buffer()
+ this@gzipFormBody.writeTo(f)
+ f.close()
+ return g.size
+ }
+
+ @Throws(IOException::class)
+ override fun writeTo(sink: BufferedSink) {
+ val buf = sink.gzip().buffer()
+ this@gzipFormBody.writeTo(buf)
+ buf.close()
+ }
+
+ override fun isOneShot(): Boolean {
+ return this@gzipFormBody.isOneShot()
+ }
+ }
+ }
+
+ fun Interceptor.Chain.toRequestBuilder(supportCompressGzip: Boolean): Request.Builder {
+ val request = request()
+ val o = request.newBuilder()
+ if (supportCompressGzip && request.body is FormBody && Utils.isCompressOutgoingTraffic) {
+ (request.body as FormBody).gzipFormBody().let {
+ o.addHeader("Content-Encoding", "gzip")
+ o.post(it)
+ }
+ }
+ return o
+ }
+
+ @Suppress("unused_parameter")
+ fun Request.Builder.vkHeader(onlyJson: Boolean): Request.Builder {
+ addHeader("X-VK-Android-Client", "new")
+ if (/*!onlyJson && */Utils.currentParser == ParserType.MSGPACK) {
+ addHeader("X-Response-Format", "msgpack")
+ }
+ return this
+ }
+
+ val DEFAULT_LOGGING_INTERCEPTOR: OkHttp3LoggingInterceptor by lazy {
+ OkHttp3LoggingInterceptor().setLevel(OkHttp3LoggingInterceptor.Level.BODY)
+ }
+
+ val UPLOAD_LOGGING_INTERCEPTOR: OkHttp3LoggingInterceptor by lazy {
+ //OkHttp3LoggingInterceptor().setLevel(OkHttp3LoggingInterceptor.Level.HEADERS)
+ OkHttp3LoggingInterceptor().setLevel(OkHttp3LoggingInterceptor.Level.BODY)
+ }
+
+ fun adjust(builder: OkHttpClient.Builder) {
+ if (Constants.IS_DEBUG) {
+ /*
+ if (Settings.get().other().currentParser == ParserType.JSON) {
+ builder.addInterceptor(DEFAULT_LOGGING_INTERCEPTOR)
+ } else {
+ builder.addInterceptor(UPLOAD_LOGGING_INTERCEPTOR)
+ }
+ */
+ builder.addInterceptor(DEFAULT_LOGGING_INTERCEPTOR)
+ }
+ }
+
+ fun adjustUpload(builder: OkHttpClient.Builder) {
+ if (Constants.IS_DEBUG) {
+ builder.addInterceptor(UPLOAD_LOGGING_INTERCEPTOR)
+ }
+ }
+
+ fun configureToIgnoreCertificates(builder: OkHttpClient.Builder) {
+ if (Settings.get().other().isValidate_tls) {
+ return
+ }
+ try {
+ val trustAllCerts: Array = arrayOf(
+ @SuppressLint("CustomX509TrustManager")
+ object : X509TrustManager {
+ @SuppressLint("TrustAllX509TrustManager")
+ @Throws(CertificateException::class)
+ override fun checkClientTrusted(
+ chain: Array,
+ authType: String?
+ ) {
+ }
+
+ @SuppressLint("TrustAllX509TrustManager")
+ @Throws(CertificateException::class)
+ override fun checkServerTrusted(
+ chain: Array,
+ authType: String?
+ ) {
+ }
+
+ override fun getAcceptedIssuers(): Array {
+ return arrayOf()
+ }
+ }
+ )
+ val sslContext: SSLContext = SSLContext.getInstance("SSL")
+ sslContext.init(null, trustAllCerts, SecureRandom())
+ val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory
+ builder.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
+ builder.hostnameVerifier { _, _ -> true }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+}
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/ICaptchaProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/ICaptchaProvider.kt
new file mode 100644
index 000000000..d1e9cf7e8
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/ICaptchaProvider.kt
@@ -0,0 +1,64 @@
+package dev.ragnarok.fenrir.api
+
+import dev.ragnarok.fenrir.api.model.Captcha
+import io.reactivex.rxjava3.core.Observable
+
+interface ICaptchaProvider {
+ /**
+ * Запросить ввод капчи
+ * После выполнения этого метода следует периодически проверять [dev.ragnarok.fenrir.api.ICaptchaProvider.lookupCode]
+ *
+ * @param sid код капчи
+ * @param captcha капча
+ */
+ fun requestCaptha(sid: String?, captcha: Captcha)
+
+ /**
+ * Отменить запрос капчи
+ *
+ * @param sid код капчи
+ */
+ fun cancel(sid: String)
+
+ /**
+ * Слушать отмену запроса капчи
+ *
+ * @return "паблишер" кода капчи
+ */
+ fun observeCanceling(): Observable
+
+ /**
+ * Проверить, не появился ли введенный текст капчи
+ *
+ * @param sid код капчи
+ * @return введенный пользователем текст с картинки
+ * @throws OutOfDateException если капча больше не обрабатывается
+ */
+ @Throws(OutOfDateException::class)
+ fun lookupCode(sid: String): String?
+
+ /**
+ * Этот "паблишер" уведомляет о том, что ожидается ввод капчи
+ * Если наблюдатель получил уведомление отсюда - должен оповестить
+ * с помощью метода [dev.ragnarok.fenrir.api.ICaptchaProvider.notifyThatCaptchaEntryActive]
+ * о том, что активен и ожадает ввода пользователя
+ *
+ * @return "паблишер" кода капчи
+ */
+ fun observeWaiting(): Observable
+
+ /**
+ * Уведомдить провайдер о том, что пользователь все еще в процессе ввода текста
+ *
+ * @param sid код капчи
+ */
+ fun notifyThatCaptchaEntryActive(sid: String)
+
+ /**
+ * Сохранение введенного пользователем текста с картинки
+ *
+ * @param sid код капчи
+ * @param code текст с картинки
+ */
+ fun enterCode(sid: String, code: String?)
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IDirectLoginSeviceProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IDirectLoginSeviceProvider.kt
new file mode 100644
index 000000000..e82b33f7a
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IDirectLoginSeviceProvider.kt
@@ -0,0 +1,8 @@
+package dev.ragnarok.fenrir.api
+
+import dev.ragnarok.fenrir.api.services.IAuthService
+import io.reactivex.rxjava3.core.Single
+
+interface IDirectLoginSeviceProvider {
+ fun provideAuthService(): Single
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/ILocalServerServiceProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/ILocalServerServiceProvider.kt
new file mode 100644
index 000000000..cceb3af15
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/ILocalServerServiceProvider.kt
@@ -0,0 +1,8 @@
+package dev.ragnarok.fenrir.api
+
+import dev.ragnarok.fenrir.api.services.ILocalServerService
+import io.reactivex.rxjava3.core.Single
+
+interface ILocalServerServiceProvider {
+ fun provideLocalServerService(): Single
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IOtherVkRetrofitProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IOtherVkRetrofitProvider.kt
new file mode 100644
index 000000000..2f78ae166
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IOtherVkRetrofitProvider.kt
@@ -0,0 +1,10 @@
+package dev.ragnarok.fenrir.api
+
+import io.reactivex.rxjava3.core.Single
+
+interface IOtherVkRetrofitProvider {
+ fun provideAuthRetrofit(): Single
+ fun provideAuthServiceRetrofit(): Single
+ fun provideLongpollRetrofit(): Single
+ fun provideLocalServerRetrofit(): Single
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IServiceProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IServiceProvider.kt
new file mode 100644
index 000000000..688aefa29
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IServiceProvider.kt
@@ -0,0 +1,11 @@
+package dev.ragnarok.fenrir.api
+
+import io.reactivex.rxjava3.core.Single
+
+interface IServiceProvider {
+ fun provideService(
+ accountId: Int,
+ serviceClass: Class,
+ vararg tokenTypes: Int
+ ): Single
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IUploadRetrofitProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IUploadRetrofitProvider.kt
new file mode 100644
index 000000000..33ec832e4
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IUploadRetrofitProvider.kt
@@ -0,0 +1,7 @@
+package dev.ragnarok.fenrir.api
+
+import io.reactivex.rxjava3.core.Single
+
+interface IUploadRetrofitProvider {
+ fun provideUploadRetrofit(): Single
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IValidateProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IValidateProvider.kt
new file mode 100644
index 000000000..47e0a116d
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IValidateProvider.kt
@@ -0,0 +1,18 @@
+package dev.ragnarok.fenrir.api
+
+import io.reactivex.rxjava3.core.Observable
+
+interface IValidateProvider {
+ fun requestValidate(url: String?)
+ fun cancel(url: String)
+ fun observeCanceling(): Observable
+
+ @Throws(OutOfDateException::class)
+ fun lookupState(url: String): Boolean
+
+ fun observeWaiting(): Observable
+
+ fun notifyThatValidateEntryActive(url: String)
+
+ fun enterState(url: String, state: Boolean)
+}
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVkMethodHttpClientFactory.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVkMethodHttpClientFactory.kt
new file mode 100644
index 000000000..d23647f73
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVkMethodHttpClientFactory.kt
@@ -0,0 +1,17 @@
+package dev.ragnarok.fenrir.api
+
+import dev.ragnarok.fenrir.AccountType
+import dev.ragnarok.fenrir.model.ProxyConfig
+import okhttp3.OkHttpClient
+
+interface IVkMethodHttpClientFactory {
+ fun createDefaultVkHttpClient(accountId: Int, config: ProxyConfig?): OkHttpClient
+ fun createCustomVkHttpClient(
+ accountId: Int,
+ token: String,
+ config: ProxyConfig?
+ ): OkHttpClient
+
+ fun createServiceVkHttpClient(config: ProxyConfig?): OkHttpClient
+ fun createRawVkApiOkHttpClient(@AccountType type: Int, config: ProxyConfig?): OkHttpClient
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVkRetrofitProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVkRetrofitProvider.kt
new file mode 100644
index 000000000..68a73e3a7
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVkRetrofitProvider.kt
@@ -0,0 +1,13 @@
+package dev.ragnarok.fenrir.api
+
+import dev.ragnarok.fenrir.AccountType
+import io.reactivex.rxjava3.core.Single
+import okhttp3.OkHttpClient
+
+interface IVkRetrofitProvider {
+ fun provideNormalRetrofit(accountId: Int): Single
+ fun provideCustomRetrofit(accountId: Int, token: String): Single
+ fun provideServiceRetrofit(): Single
+ fun provideNormalHttpClient(accountId: Int): Single
+ fun provideRawHttpClient(@AccountType type: Int): Single
+}
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/NeedValidationException.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/NeedValidationException.kt
new file mode 100644
index 000000000..9261bc6f7
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/NeedValidationException.kt
@@ -0,0 +1,7 @@
+package dev.ragnarok.fenrir.api
+
+class NeedValidationException(
+ val validationType: String?,
+ val validationURL: String?,
+ val sid: String?
+) : Exception()
\ No newline at end of file
diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/OtherVkRetrofitProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/OtherVkRetrofitProvider.kt
new file mode 100644
index 000000000..200307d67
--- /dev/null
+++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/OtherVkRetrofitProvider.kt
@@ -0,0 +1,204 @@
+package dev.ragnarok.fenrir.api
+
+import android.annotation.SuppressLint
+import dev.ragnarok.fenrir.AccountType
+import dev.ragnarok.fenrir.Constants
+import dev.ragnarok.fenrir.Constants.USER_AGENT
+import dev.ragnarok.fenrir.api.HttpLoggerAndParser.toRequestBuilder
+import dev.ragnarok.fenrir.api.HttpLoggerAndParser.vkHeader
+import dev.ragnarok.fenrir.api.RetrofitWrapper.Companion.wrap
+import dev.ragnarok.fenrir.kJson
+import dev.ragnarok.fenrir.nonNullNoEmpty
+import dev.ragnarok.fenrir.settings.IProxySettings
+import dev.ragnarok.fenrir.settings.Settings
+import dev.ragnarok.fenrir.util.UncompressDefaultInterceptor
+import dev.ragnarok.fenrir.util.Utils
+import dev.ragnarok.fenrir.util.serializeble.msgpack.MsgPack
+import dev.ragnarok.fenrir.util.serializeble.retrofit.kotlinx.serialization.jsonMsgPackConverterFactory
+import dev.ragnarok.fenrir.util.serializeble.retrofit.rxjava3.RxJava3CallAdapterFactory
+import io.reactivex.rxjava3.core.Single
+import okhttp3.FormBody
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import java.util.concurrent.TimeUnit
+
+class OtherVkRetrofitProvider @SuppressLint("CheckResult") constructor(private val proxySettings: IProxySettings) :
+ IOtherVkRetrofitProvider {
+ private val longpollRetrofitLock = Any()
+ private val localServerRetrofitLock = Any()
+ private var longpollRetrofitInstance: RetrofitWrapper? = null
+ private var localServerRetrofitInstance: RetrofitWrapper? = null
+ private fun onProxySettingsChanged() {
+ synchronized(longpollRetrofitLock) {
+ if (longpollRetrofitInstance != null) {
+ longpollRetrofitInstance?.cleanup()
+ longpollRetrofitInstance = null
+ }
+ }
+ synchronized(localServerRetrofitLock) {
+ if (localServerRetrofitInstance != null) {
+ localServerRetrofitInstance?.cleanup()
+ localServerRetrofitInstance = null
+ }
+ }
+ }
+
+ override fun provideAuthRetrofit(): Single {
+ return Single.fromCallable {
+ val builder: OkHttpClient.Builder = OkHttpClient.Builder()
+ .readTimeout(30, TimeUnit.SECONDS)
+ .addInterceptor(Interceptor { chain: Interceptor.Chain ->
+ val request =
+ chain.toRequestBuilder(false).vkHeader(true)
+ .addHeader(
+ "User-Agent", USER_AGENT(
+ Constants.DEFAULT_ACCOUNT_TYPE
+ )
+ ).build()
+ chain.proceed(request)
+ })
+ .addInterceptor(UncompressDefaultInterceptor)
+ ProxyUtil.applyProxyConfig(builder, proxySettings.activeProxy)
+ HttpLoggerAndParser.adjust(builder)
+ HttpLoggerAndParser.configureToIgnoreCertificates(builder)
+ val retrofit = Retrofit.Builder()
+ .baseUrl("https://" + Settings.get().other().get_Auth_Domain() + "/")
+ .addConverterFactory(KCONVERTER_FACTORY)
+ .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
+ .client(builder.build())
+ .build()
+ wrap(retrofit, false)
+ }
+ }
+
+ override fun provideAuthServiceRetrofit(): Single