Loading build.gradle +1 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ buildscript { ext.androidx_emoji_version = '1.0.0' ext.androidx_exifinterface_version = '1.0.0' ext.androidx_testrunner_version = '1.1.0-alpha3' ext.androidx_viewpager_version = '1.0.0-alpha01' ext.autodispose_version = '0.7.0' ext.conductor_version = '2.1.5' ext.dagger_version = "2.16" Loading presentation/build.gradle +2 −1 Original line number Diff line number Diff line Loading @@ -113,8 +113,9 @@ dependencies { implementation "androidx.appcompat:appcompat:$androidx_appcompat_version" implementation "androidx.constraintlayout:constraintlayout:$androidx_constraintlayout_version" implementation "androidx.core:core-ktx:$androidx_core_version" implementation "com.google.android.material:material:$material_version" implementation "androidx.emoji:emoji-appcompat:$androidx_emoji_version" implementation "androidx.viewpager2:viewpager2:$androidx_viewpager_version" implementation "com.google.android.material:material:$material_version" // conductor implementation "com.bluelinelabs:conductor:$conductor_version" Loading presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryActivity.kt +27 −12 Original line number Diff line number Diff line Loading @@ -24,8 +24,11 @@ import android.view.MenuItem import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import com.moez.QKSMS.R import com.moez.QKSMS.common.base.QkActivity import com.moez.QKSMS.common.util.DateFormatter import com.moez.QKSMS.common.util.extensions.addOnPageChangeListener import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.model.MmsPart Loading @@ -38,9 +41,12 @@ import javax.inject.Inject class GalleryActivity : QkActivity(), GalleryView { @Inject lateinit var dateFormatter: DateFormatter @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @Inject lateinit var pagerAdapter: GalleryPagerAdapter val partId by lazy { intent.getLongExtra("partId", 0L) } private val optionsItemSubject: Subject<Int> = PublishSubject.create() private val pageChangedSubject: Subject<MmsPart> = PublishSubject.create() private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[GalleryViewModel::class.java] } Loading @@ -52,20 +58,29 @@ class GalleryActivity : QkActivity(), GalleryView { showBackButton(true) viewModel.bindView(this) pagerAdapter.setInitialPositionHandler = { position -> pager.setCurrentItem(position, false) // The ViewPager's page change listener isn't called if the initial position is 0 if (position == 0) { pageChanged(0) pager.adapter = pagerAdapter pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { this@GalleryActivity.onPageSelected(position) } }) pagerAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onChanged() { pagerAdapter.data?.takeIf { pagerAdapter.itemCount > 0 } ?.indexOfFirst { part -> part.id == partId } ?.let { index -> onPageSelected(index) pager.setCurrentItem(index, false) pagerAdapter.unregisterAdapterDataObserver(this) } pager.adapter = pagerAdapter pager.addOnPageChangeListener(this::pageChanged) } }) } private fun pageChanged(position: Int) { toolbarSubtitle.text = pagerAdapter.getPageTitle(position) fun onPageSelected(position: Int) { toolbarSubtitle.text = pagerAdapter.getItem(position)?.messages?.firstOrNull()?.date ?.let(dateFormatter::getDetailedTimestamp) toolbarSubtitle.isVisible = toolbarTitle.text.isNotBlank() pagerAdapter.getItem(position)?.run(pageChangedSubject::onNext) Loading @@ -75,7 +90,7 @@ class GalleryActivity : QkActivity(), GalleryView { toolbar.setVisible(state.navigationVisible) title = state.title pagerAdapter.parts = state.parts pagerAdapter.updateData(state.parts) } override fun optionsItemSelected(): Observable<Int> = optionsItemSubject Loading presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryActivityModule.kt +1 −1 Original line number Diff line number Diff line Loading @@ -34,7 +34,7 @@ class GalleryActivityModule { @Provides @Named("partId") fun providePartId(activity: GalleryActivity): Long = activity.intent.getLongExtra("partId", 0L) fun providePartId(activity: GalleryActivity): Long = activity.partId @Provides @IntoMap Loading presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryPagerAdapter.kt +38 −73 Original line number Diff line number Diff line Loading @@ -22,7 +22,6 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.viewpager.widget.PagerAdapter import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayerFactory import com.google.android.exoplayer2.source.ExtractorMediaSource Loading @@ -32,57 +31,36 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory import com.google.android.exoplayer2.util.Util import com.google.android.mms.ContentType import com.moez.QKSMS.R import com.moez.QKSMS.common.util.DateFormatter import com.moez.QKSMS.common.base.QkRealmAdapter import com.moez.QKSMS.common.base.QkViewHolder import com.moez.QKSMS.extensions.isImage import com.moez.QKSMS.extensions.isVideo import com.moez.QKSMS.model.MmsPart import com.moez.QKSMS.util.GlideApp import io.reactivex.disposables.CompositeDisposable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import io.realm.RealmResults import kotlinx.android.synthetic.main.gallery_image_page.view.* import kotlinx.android.synthetic.main.gallery_video_page.view.* import java.util.* import javax.inject.Inject import javax.inject.Named class GalleryPagerAdapter @Inject constructor( context: Context, @Named("partId") private val partId: Long, private val dateFormatter: DateFormatter ) : PagerAdapter() { class GalleryPagerAdapter @Inject constructor(private val context: Context) : QkRealmAdapter<MmsPart>() { var parts: RealmResults<MmsPart>? = null set(value) { if (field === value) return field = value field?.asFlowable() ?.filter { it.isLoaded } ?.subscribe { notifyDataSetChanged() } ?.run(disposables::add) companion object { private const val VIEW_TYPE_INVALID = 0 private const val VIEW_TYPE_IMAGE = 1 private const val VIEW_TYPE_VIDEO = 2 } val clicks: Subject<View> = PublishSubject.create() /** * The Adapter isn't able to set the position itself, so it's owner must apply the initial * position once the data is loaded */ var setInitialPositionHandler: ((Int) -> Unit)? = null private var loaded = false private val contentResolver = context.contentResolver private val disposables = CompositeDisposable() private val exoPlayers = Collections.newSetFromMap(WeakHashMap<ExoPlayer?, Boolean>()) override fun instantiateItem(container: ViewGroup, position: Int): Any { val part = getItem(position) val inflater = LayoutInflater.from(container.context) return when { part?.isImage() == true -> inflater.inflate(R.layout.gallery_image_page, container, false).apply { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val inflater = LayoutInflater.from(parent.context) return QkViewHolder(when (viewType) { VIEW_TYPE_IMAGE -> inflater.inflate(R.layout.gallery_image_page, parent, false).apply { // When calling the public setter, it doesn't allow the midscale to be the same as the // maxscale or the minscale. We don't want 3 levels and we don't want to modify the library Loading @@ -101,71 +79,58 @@ class GalleryPagerAdapter @Inject constructor( setFloat(image.attacher, 3f) } } } VIEW_TYPE_VIDEO -> inflater.inflate(R.layout.gallery_video_page, parent, false) else -> inflater.inflate(R.layout.gallery_invalid_page, parent, false) }.apply { setOnClickListener(clicks::onNext) }) } override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val part = getItem(position)!! val view = holder.containerView when (getItemViewType(position)) { VIEW_TYPE_IMAGE -> { // We need to explicitly request a gif from glide for animations to work when (part.getUri().let(contentResolver::getType)) { ContentType.IMAGE_GIF -> GlideApp.with(context) .asGif() .load(part.getUri()) .into(image) .into(view.image) else -> GlideApp.with(context) .asBitmap() .load(part.getUri()) .into(image) .into(view.image) } container.addView(this) } part?.isVideo() == true -> inflater.inflate(R.layout.gallery_video_page, container, false).apply { VIEW_TYPE_VIDEO -> { val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(null) val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory) val exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector) video.player = exoPlayer view.video.player = exoPlayer exoPlayers.add(exoPlayer) val dataSourceFactory = DefaultDataSourceFactory(context, Util.getUserAgent(context, "QKSMS")) val videoSource = ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(part.getUri()) exoPlayer?.prepare(videoSource) container.addView(this) } else -> inflater.inflate(R.layout.gallery_invalid_page, container, false).apply { container.addView(this) } }.apply { setOnClickListener(clicks::onNext) } } override fun getPageTitle(position: Int): CharSequence? { return getItem(position)?.messages?.firstOrNull()?.date?.let(dateFormatter::getDetailedTimestamp) } override fun isViewFromObject(view: View, `object`: Any): Boolean { return view == `object` } override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { container.removeView(`object` as? View) } override fun getCount() = parts?.size ?: 0 fun getItem(position: Int): MmsPart? = parts?.get(position) override fun notifyDataSetChanged() { super.notifyDataSetChanged() if (!loaded && parts?.isLoaded == true) { loaded = true parts?.indexOfFirst { it.id == partId } ?.let { setInitialPositionHandler?.invoke(it) } override fun getItemViewType(position: Int): Int { val part = getItem(position) return when { part?.isImage() == true -> VIEW_TYPE_IMAGE part?.isVideo() == true -> VIEW_TYPE_VIDEO else -> VIEW_TYPE_INVALID } } fun destroy() { disposables.dispose() exoPlayers.forEach { exoPlayer -> exoPlayer?.release() } } Loading Loading
build.gradle +1 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ buildscript { ext.androidx_emoji_version = '1.0.0' ext.androidx_exifinterface_version = '1.0.0' ext.androidx_testrunner_version = '1.1.0-alpha3' ext.androidx_viewpager_version = '1.0.0-alpha01' ext.autodispose_version = '0.7.0' ext.conductor_version = '2.1.5' ext.dagger_version = "2.16" Loading
presentation/build.gradle +2 −1 Original line number Diff line number Diff line Loading @@ -113,8 +113,9 @@ dependencies { implementation "androidx.appcompat:appcompat:$androidx_appcompat_version" implementation "androidx.constraintlayout:constraintlayout:$androidx_constraintlayout_version" implementation "androidx.core:core-ktx:$androidx_core_version" implementation "com.google.android.material:material:$material_version" implementation "androidx.emoji:emoji-appcompat:$androidx_emoji_version" implementation "androidx.viewpager2:viewpager2:$androidx_viewpager_version" implementation "com.google.android.material:material:$material_version" // conductor implementation "com.bluelinelabs:conductor:$conductor_version" Loading
presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryActivity.kt +27 −12 Original line number Diff line number Diff line Loading @@ -24,8 +24,11 @@ import android.view.MenuItem import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import com.moez.QKSMS.R import com.moez.QKSMS.common.base.QkActivity import com.moez.QKSMS.common.util.DateFormatter import com.moez.QKSMS.common.util.extensions.addOnPageChangeListener import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.model.MmsPart Loading @@ -38,9 +41,12 @@ import javax.inject.Inject class GalleryActivity : QkActivity(), GalleryView { @Inject lateinit var dateFormatter: DateFormatter @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @Inject lateinit var pagerAdapter: GalleryPagerAdapter val partId by lazy { intent.getLongExtra("partId", 0L) } private val optionsItemSubject: Subject<Int> = PublishSubject.create() private val pageChangedSubject: Subject<MmsPart> = PublishSubject.create() private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[GalleryViewModel::class.java] } Loading @@ -52,20 +58,29 @@ class GalleryActivity : QkActivity(), GalleryView { showBackButton(true) viewModel.bindView(this) pagerAdapter.setInitialPositionHandler = { position -> pager.setCurrentItem(position, false) // The ViewPager's page change listener isn't called if the initial position is 0 if (position == 0) { pageChanged(0) pager.adapter = pagerAdapter pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { this@GalleryActivity.onPageSelected(position) } }) pagerAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onChanged() { pagerAdapter.data?.takeIf { pagerAdapter.itemCount > 0 } ?.indexOfFirst { part -> part.id == partId } ?.let { index -> onPageSelected(index) pager.setCurrentItem(index, false) pagerAdapter.unregisterAdapterDataObserver(this) } pager.adapter = pagerAdapter pager.addOnPageChangeListener(this::pageChanged) } }) } private fun pageChanged(position: Int) { toolbarSubtitle.text = pagerAdapter.getPageTitle(position) fun onPageSelected(position: Int) { toolbarSubtitle.text = pagerAdapter.getItem(position)?.messages?.firstOrNull()?.date ?.let(dateFormatter::getDetailedTimestamp) toolbarSubtitle.isVisible = toolbarTitle.text.isNotBlank() pagerAdapter.getItem(position)?.run(pageChangedSubject::onNext) Loading @@ -75,7 +90,7 @@ class GalleryActivity : QkActivity(), GalleryView { toolbar.setVisible(state.navigationVisible) title = state.title pagerAdapter.parts = state.parts pagerAdapter.updateData(state.parts) } override fun optionsItemSelected(): Observable<Int> = optionsItemSubject Loading
presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryActivityModule.kt +1 −1 Original line number Diff line number Diff line Loading @@ -34,7 +34,7 @@ class GalleryActivityModule { @Provides @Named("partId") fun providePartId(activity: GalleryActivity): Long = activity.intent.getLongExtra("partId", 0L) fun providePartId(activity: GalleryActivity): Long = activity.partId @Provides @IntoMap Loading
presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryPagerAdapter.kt +38 −73 Original line number Diff line number Diff line Loading @@ -22,7 +22,6 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.viewpager.widget.PagerAdapter import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayerFactory import com.google.android.exoplayer2.source.ExtractorMediaSource Loading @@ -32,57 +31,36 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory import com.google.android.exoplayer2.util.Util import com.google.android.mms.ContentType import com.moez.QKSMS.R import com.moez.QKSMS.common.util.DateFormatter import com.moez.QKSMS.common.base.QkRealmAdapter import com.moez.QKSMS.common.base.QkViewHolder import com.moez.QKSMS.extensions.isImage import com.moez.QKSMS.extensions.isVideo import com.moez.QKSMS.model.MmsPart import com.moez.QKSMS.util.GlideApp import io.reactivex.disposables.CompositeDisposable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import io.realm.RealmResults import kotlinx.android.synthetic.main.gallery_image_page.view.* import kotlinx.android.synthetic.main.gallery_video_page.view.* import java.util.* import javax.inject.Inject import javax.inject.Named class GalleryPagerAdapter @Inject constructor( context: Context, @Named("partId") private val partId: Long, private val dateFormatter: DateFormatter ) : PagerAdapter() { class GalleryPagerAdapter @Inject constructor(private val context: Context) : QkRealmAdapter<MmsPart>() { var parts: RealmResults<MmsPart>? = null set(value) { if (field === value) return field = value field?.asFlowable() ?.filter { it.isLoaded } ?.subscribe { notifyDataSetChanged() } ?.run(disposables::add) companion object { private const val VIEW_TYPE_INVALID = 0 private const val VIEW_TYPE_IMAGE = 1 private const val VIEW_TYPE_VIDEO = 2 } val clicks: Subject<View> = PublishSubject.create() /** * The Adapter isn't able to set the position itself, so it's owner must apply the initial * position once the data is loaded */ var setInitialPositionHandler: ((Int) -> Unit)? = null private var loaded = false private val contentResolver = context.contentResolver private val disposables = CompositeDisposable() private val exoPlayers = Collections.newSetFromMap(WeakHashMap<ExoPlayer?, Boolean>()) override fun instantiateItem(container: ViewGroup, position: Int): Any { val part = getItem(position) val inflater = LayoutInflater.from(container.context) return when { part?.isImage() == true -> inflater.inflate(R.layout.gallery_image_page, container, false).apply { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val inflater = LayoutInflater.from(parent.context) return QkViewHolder(when (viewType) { VIEW_TYPE_IMAGE -> inflater.inflate(R.layout.gallery_image_page, parent, false).apply { // When calling the public setter, it doesn't allow the midscale to be the same as the // maxscale or the minscale. We don't want 3 levels and we don't want to modify the library Loading @@ -101,71 +79,58 @@ class GalleryPagerAdapter @Inject constructor( setFloat(image.attacher, 3f) } } } VIEW_TYPE_VIDEO -> inflater.inflate(R.layout.gallery_video_page, parent, false) else -> inflater.inflate(R.layout.gallery_invalid_page, parent, false) }.apply { setOnClickListener(clicks::onNext) }) } override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val part = getItem(position)!! val view = holder.containerView when (getItemViewType(position)) { VIEW_TYPE_IMAGE -> { // We need to explicitly request a gif from glide for animations to work when (part.getUri().let(contentResolver::getType)) { ContentType.IMAGE_GIF -> GlideApp.with(context) .asGif() .load(part.getUri()) .into(image) .into(view.image) else -> GlideApp.with(context) .asBitmap() .load(part.getUri()) .into(image) .into(view.image) } container.addView(this) } part?.isVideo() == true -> inflater.inflate(R.layout.gallery_video_page, container, false).apply { VIEW_TYPE_VIDEO -> { val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(null) val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory) val exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector) video.player = exoPlayer view.video.player = exoPlayer exoPlayers.add(exoPlayer) val dataSourceFactory = DefaultDataSourceFactory(context, Util.getUserAgent(context, "QKSMS")) val videoSource = ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(part.getUri()) exoPlayer?.prepare(videoSource) container.addView(this) } else -> inflater.inflate(R.layout.gallery_invalid_page, container, false).apply { container.addView(this) } }.apply { setOnClickListener(clicks::onNext) } } override fun getPageTitle(position: Int): CharSequence? { return getItem(position)?.messages?.firstOrNull()?.date?.let(dateFormatter::getDetailedTimestamp) } override fun isViewFromObject(view: View, `object`: Any): Boolean { return view == `object` } override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { container.removeView(`object` as? View) } override fun getCount() = parts?.size ?: 0 fun getItem(position: Int): MmsPart? = parts?.get(position) override fun notifyDataSetChanged() { super.notifyDataSetChanged() if (!loaded && parts?.isLoaded == true) { loaded = true parts?.indexOfFirst { it.id == partId } ?.let { setInitialPositionHandler?.invoke(it) } override fun getItemViewType(position: Int): Int { val part = getItem(position) return when { part?.isImage() == true -> VIEW_TYPE_IMAGE part?.isVideo() == true -> VIEW_TYPE_VIDEO else -> VIEW_TYPE_INVALID } } fun destroy() { disposables.dispose() exoPlayers.forEach { exoPlayer -> exoPlayer?.release() } } Loading