diff --git a/.idea/modules.xml b/.idea/modules.xml index d47dcd74..19c2e41f 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,9 +2,8 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/ChannelDetailActivity.java b/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/ChannelDetailActivity.java deleted file mode 100644 index 1b56ab24..00000000 --- a/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/ChannelDetailActivity.java +++ /dev/null @@ -1,386 +0,0 @@ -package de.christinecoenen.code.zapp.app.livestream.ui.detail; - -import android.annotation.SuppressLint; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.pm.ActivityInfo; -import android.graphics.PorterDuff; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.Window; -import android.widget.ProgressBar; - -import androidx.annotation.NonNull; -import androidx.appcompat.widget.Toolbar; -import androidx.viewpager.widget.ViewPager; - -import de.christinecoenen.code.zapp.R; -import de.christinecoenen.code.zapp.app.ZappApplicationBase; -import de.christinecoenen.code.zapp.app.livestream.ui.views.ProgramInfoViewBase; -import de.christinecoenen.code.zapp.app.player.BackgroundPlayerService; -import de.christinecoenen.code.zapp.app.player.Player; -import de.christinecoenen.code.zapp.app.player.VideoInfo; -import de.christinecoenen.code.zapp.app.settings.repository.SettingsRepository; -import de.christinecoenen.code.zapp.databinding.ActivityChannelDetailBinding; -import de.christinecoenen.code.zapp.models.channels.ChannelModel; -import de.christinecoenen.code.zapp.models.channels.IChannelList; -import de.christinecoenen.code.zapp.utils.system.MultiWindowHelper; -import de.christinecoenen.code.zapp.utils.system.ShortcutHelper; -import de.christinecoenen.code.zapp.utils.video.SwipeablePlayerView; -import de.christinecoenen.code.zapp.utils.view.ClickableViewPager; -import de.christinecoenen.code.zapp.utils.view.ColorHelper; -import de.christinecoenen.code.zapp.utils.view.FullscreenActivity; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; -import timber.log.Timber; - -public class ChannelDetailActivity extends FullscreenActivity implements StreamPageFragment.Listener { - - private static final String EXTRA_CHANNEL_ID = "de.christinecoenen.code.zapp.EXTRA_CHANNEL_ID"; - - private Toolbar toolbar; - private ClickableViewPager viewPager; - private SwipeablePlayerView videoView; - private ProgressBar progressView; - private ProgramInfoViewBase programInfoView; - - private int playStreamDelayMillis; - - private final Handler playHandler = new Handler(); - private ChannelDetailAdapter channelDetailAdapter; - private ChannelModel currentChannel; - private Window window; - private IChannelList channelList; - private Player player; - private BackgroundPlayerService.Binder binder; - private SettingsRepository settings; - private final CompositeDisposable disposable = new CompositeDisposable(); - - private final Runnable playRunnable = this::play; - - private final ChannelDetailAdapter.Listener channelDetailListener = - new ChannelDetailAdapter.Listener() { - @Override - public void onItemSelected(ChannelModel channel) { - currentChannel = channel; - setTitle(channel.getName()); - setColor(channel.getColor()); - - playDelayed(); - programInfoView.setChannel(channel); - - ShortcutHelper.reportShortcutUsageGuarded(ChannelDetailActivity.this, channel.getId()); - } - }; - - private final ViewPager.OnPageChangeListener onPageChangeListener = - new ViewPager.SimpleOnPageChangeListener() { - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - int color1 = channelDetailAdapter.getChannel(position).getColor(); - - if (positionOffset == 0) { - setColor(color1); - } else { - int color2 = channelDetailAdapter.getChannel(position + 1).getColor(); - int color = ColorHelper.interpolate(positionOffset, color1, color2); - setColor(color); - } - } - }; - - private final ServiceConnection backgroundPlayerServiceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName componentName, IBinder service) { - binder = (BackgroundPlayerService.Binder) service; - binder.setForegroundActivityIntent(getIntent()); - player = binder.getPlayer(); - player.setView(videoView); - - Disposable bufferingDisposable = player.isBuffering() - .subscribe(ChannelDetailActivity.this::onBufferingChanged, Timber::e); - Disposable errorDisposable = player.getErrorResourceId() - .subscribe(ChannelDetailActivity.this::onVideoError, Timber::e); - disposable.addAll(bufferingDisposable, errorDisposable); - - channelDetailListener.onItemSelected(currentChannel); - - binder.movePlaybackToForeground(); - } - - @Override - public void onServiceDisconnected(ComponentName componentName) { - player.pause(); - } - }; - - public static Intent getStartIntent(Context context, String channelId) { - Intent intent = new Intent(context, ChannelDetailActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(EXTRA_CHANNEL_ID, channelId); - return intent; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - ActivityChannelDetailBinding binding = ActivityChannelDetailBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - toolbar = binding.toolbar; - viewPager = binding.viewpagerChannels; - videoView = binding.video; - programInfoView = binding.programInfo; - progressView = binding.progressbarVideo; - - playStreamDelayMillis = getResources().getInteger(R.integer.activity_channel_detail_play_stream_delay_millis); - - setSupportActionBar(toolbar); - window = getWindow(); - - if (getSupportActionBar() != null) { - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - channelList = ((ZappApplicationBase) getApplication()).getChannelRepository().getChannelList(); - settings = new SettingsRepository(this); - - // pager - channelDetailAdapter = new ChannelDetailAdapter( - getSupportFragmentManager(), channelList, channelDetailListener); - viewPager.setAdapter(channelDetailAdapter); - viewPager.addOnPageChangeListener(onPageChangeListener); - viewPager.setOnClickListener(view -> contentView.performClick()); - viewPager.setOnTouchListener(this::onPagerTouch); - videoView.setTouchOverlay(viewPager); - - parseIntent(getIntent()); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - // called when coming back from picture in picture mode - parseIntent(intent); - } - - @Override - protected void onStart() { - super.onStart(); - if (MultiWindowHelper.isInsideMultiWindow(this)) { - resumeActivity(); - } - } - - @SuppressLint("SourceLockedOrientationActivity") - @Override - protected void onResume() { - super.onResume(); - if (!MultiWindowHelper.isInsideMultiWindow(this)) { - resumeActivity(); - } - - if (settings.getLockVideosInLandcapeFormat()) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); - } else { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); - } - } - - @Override - protected void onPause() { - super.onPause(); - if (!MultiWindowHelper.isInsideMultiWindow(this)) { - pauseActivity(); - } - } - - @Override - protected void onStop() { - super.onStop(); - pauseActivity(); - } - - @Override - public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) { - super.onPictureInPictureModeChanged(isInPictureInPictureMode); - if (isInPictureInPictureMode) { - hide(); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.activity_channel_detail, menu); - - if (!MultiWindowHelper.supportsPictureInPictureMode(this)) { - menu.removeItem(R.id.menu_pip); - } - - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_share: - startActivity(Intent.createChooser(currentChannel.getVideoShareIntent(), getString(R.string.action_share))); - return true; - case R.id.menu_play_in_background: - binder.movePlaybackToBackground(); - finish(); - return true; - case R.id.menu_pip: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - enterPictureInPictureMode(); - } - return true; - case android.R.id.home: - finish(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - boolean handled = false; - - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - handled = prevChannel(); - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - handled = nextChannel(); - break; - } - - return handled || super.onKeyUp(keyCode, event); - } - - private void onVideoError(Integer messageResourceId) { - if (messageResourceId == null || messageResourceId == -1) { - return; - } - - player.pause(); - progressView.setVisibility(View.GONE); - - if (channelDetailAdapter.getCurrentFragment() != null) { - channelDetailAdapter.getCurrentFragment().onVideoError(getString(messageResourceId)); - } - } - - private void onBufferingChanged(boolean isBuffering) { - if (isBuffering) { - progressView.setVisibility(View.VISIBLE); - } else if (!player.isIdle()) { - progressView.setVisibility(View.INVISIBLE); - channelDetailAdapter.getCurrentFragment().onVideoStart(); - } - } - - @Override - public void onErrorViewClicked() { - player.recreate(); - player.resume(); - } - - private boolean onPagerTouch(View view, MotionEvent motionEvent) { - delayHide(); - return false; - } - - private void parseIntent(Intent intent) { - Bundle extras = intent.getExtras(); - //noinspection ConstantConditions - String channelId = extras.getString(EXTRA_CHANNEL_ID); - int channelPosition = channelList.indexOf(channelId); - - viewPager.removeOnPageChangeListener(onPageChangeListener); - viewPager.setCurrentItem(channelPosition); - viewPager.addOnPageChangeListener(onPageChangeListener); - } - - private void pauseActivity() { - disposable.clear(); - try { - unbindService(backgroundPlayerServiceConnection); - } catch (IllegalArgumentException ignored) { - - } - } - - private void resumeActivity() { - programInfoView.resume(); - BackgroundPlayerService.bind(this, backgroundPlayerServiceConnection); - } - - private void playDelayed() { - if (player != null) { - player.pause(); - } - - playHandler.removeCallbacks(playRunnable); - playHandler.postDelayed(playRunnable, playStreamDelayMillis); - } - - private void play() { - if (currentChannel == null || binder == null) { - return; - } - - Intent currentIntent = ChannelDetailActivity.getStartIntent(this, currentChannel.getId()); - binder.setForegroundActivityIntent(currentIntent); - - Timber.d("play: %s", currentChannel.getName()); - player.load(VideoInfo.fromChannel(currentChannel)); - player.resume(); - } - - private boolean prevChannel() { - int nextItemIndex = viewPager.getCurrentItem() - 1; - - if (nextItemIndex < 0) { - return false; - } else { - delayHide(); - viewPager.setCurrentItem(nextItemIndex, true); - return true; - } - } - - private boolean nextChannel() { - int nextItemIndex = viewPager.getCurrentItem() + 1; - - if (nextItemIndex == channelDetailAdapter.getCount()) { - return false; - } else { - delayHide(); - viewPager.setCurrentItem(nextItemIndex, true); - return true; - } - } - - private void setColor(int color) { - progressView.getIndeterminateDrawable().setColorFilter(color, PorterDuff.Mode.SRC_IN); - toolbar.setBackgroundColor(color); - - int colorDarker = ColorHelper.darker(color, 0.075f); - window.setStatusBarColor(colorDarker); - - int colorAlpha = ColorHelper.darker(ColorHelper.withAlpha(color, 150), 0.25f); - controlsView.setBackgroundColor(colorAlpha); - } -} diff --git a/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/ChannelDetailActivity.kt b/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/ChannelDetailActivity.kt new file mode 100644 index 00000000..72f47d63 --- /dev/null +++ b/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/ChannelDetailActivity.kt @@ -0,0 +1,354 @@ +package de.christinecoenen.code.zapp.app.livestream.ui.detail + +import android.annotation.SuppressLint +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.content.pm.ActivityInfo +import android.graphics.PorterDuff +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.IBinder +import android.view.* +import androidx.core.view.isVisible +import androidx.viewpager.widget.ViewPager.OnPageChangeListener +import androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener +import de.christinecoenen.code.zapp.R +import de.christinecoenen.code.zapp.app.ZappApplicationBase +import de.christinecoenen.code.zapp.app.player.BackgroundPlayerService +import de.christinecoenen.code.zapp.app.player.BackgroundPlayerService.Companion.bind +import de.christinecoenen.code.zapp.app.player.Player +import de.christinecoenen.code.zapp.app.player.VideoInfo.Companion.fromChannel +import de.christinecoenen.code.zapp.app.settings.repository.SettingsRepository +import de.christinecoenen.code.zapp.databinding.ActivityChannelDetailBinding +import de.christinecoenen.code.zapp.models.channels.ChannelModel +import de.christinecoenen.code.zapp.models.channels.IChannelList +import de.christinecoenen.code.zapp.utils.system.MultiWindowHelper.isInsideMultiWindow +import de.christinecoenen.code.zapp.utils.system.MultiWindowHelper.supportsPictureInPictureMode +import de.christinecoenen.code.zapp.utils.system.ShortcutHelper.reportShortcutUsageGuarded +import de.christinecoenen.code.zapp.utils.view.ColorHelper.darker +import de.christinecoenen.code.zapp.utils.view.ColorHelper.interpolate +import de.christinecoenen.code.zapp.utils.view.ColorHelper.withAlpha +import de.christinecoenen.code.zapp.utils.view.FullscreenActivity +import io.reactivex.disposables.CompositeDisposable +import timber.log.Timber + +class ChannelDetailActivity : FullscreenActivity(), StreamPageFragment.Listener { + + companion object { + + private const val EXTRA_CHANNEL_ID = "de.christinecoenen.code.zapp.EXTRA_CHANNEL_ID" + + @JvmStatic + fun getStartIntent(context: Context?, channelId: String?): Intent { + return Intent(context, ChannelDetailActivity::class.java).apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_CHANNEL_ID, channelId) + } + } + + } + + private lateinit var binding: ActivityChannelDetailBinding + private lateinit var channelList: IChannelList + private lateinit var settings: SettingsRepository + private lateinit var channelDetailAdapter: ChannelDetailAdapter + + private val disposable = CompositeDisposable() + private val playRunnable = Runnable { play() } + private val playHandler = Handler() + + private var playStreamDelayMillis = 0 + private var currentChannel: ChannelModel? = null + private var player: Player? = null + private var binder: BackgroundPlayerService.Binder? = null + + private val channelDetailListener: ChannelDetailAdapter.Listener = object : ChannelDetailAdapter.Listener { + override fun onItemSelected(channel: ChannelModel) { + currentChannel = channel + title = channel.name + + setColor(channel.color) + playDelayed() + + binding.programInfo.setChannel(channel) + + reportShortcutUsageGuarded(this@ChannelDetailActivity, channel.id) + } + } + + private val onPageChangeListener: OnPageChangeListener = object : SimpleOnPageChangeListener() { + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + val currentChannelColor = channelDetailAdapter.getChannel(position).color + + if (positionOffset == 0f) { + setColor(currentChannelColor) + } else { + val nextChannelColor = channelDetailAdapter.getChannel(position + 1).color + val interpolatedColor = interpolate(positionOffset, currentChannelColor, nextChannelColor) + setColor(interpolatedColor) + } + } + } + + private val backgroundPlayerServiceConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected(componentName: ComponentName, service: IBinder) { + binder = service as BackgroundPlayerService.Binder + binder!!.setForegroundActivityIntent(intent) + + player = binder!!.getPlayer() + player!!.setView(binding.video) + + player!!.isBuffering + .subscribe(::onBufferingChanged, Timber::e) + .also(disposable::add) + + player!!.errorResourceId + .subscribe(::onVideoError, Timber::e) + .also(disposable::add) + + channelDetailListener.onItemSelected(currentChannel!!) + + binder!!.movePlaybackToForeground() + } + + override fun onServiceDisconnected(componentName: ComponentName) { + player?.pause() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityChannelDetailBinding.inflate(layoutInflater) + setContentView(binding.root) + + playStreamDelayMillis = resources.getInteger(R.integer.activity_channel_detail_play_stream_delay_millis) + + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + channelList = (application as ZappApplicationBase).channelRepository.getChannelList() + settings = SettingsRepository(this) + + // pager + channelDetailAdapter = ChannelDetailAdapter( + supportFragmentManager, channelList, channelDetailListener) + + binding.viewpager.adapter = channelDetailAdapter + binding.viewpager.addOnPageChangeListener(onPageChangeListener) + binding.viewpager.setOnClickListener { contentView.performClick() } + binding.viewpager.setOnTouchListener(::onPagerTouch) + + binding.video.setTouchOverlay(binding.viewpager) + + parseIntent(intent) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + + // called when coming back from picture in picture mode + parseIntent(intent) + } + + override fun onStart() { + super.onStart() + + if (isInsideMultiWindow(this)) { + resumeActivity() + } + } + + @SuppressLint("SourceLockedOrientationActivity") + override fun onResume() { + super.onResume() + + if (!isInsideMultiWindow(this)) { + resumeActivity() + } + + requestedOrientation = if (settings.lockVideosInLandcapeFormat) { + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + } else { + ActivityInfo.SCREEN_ORIENTATION_SENSOR + } + } + + override fun onPause() { + super.onPause() + + if (!isInsideMultiWindow(this)) { + pauseActivity() + } + } + + override fun onStop() { + super.onStop() + + pauseActivity() + } + + override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode) + if (isInPictureInPictureMode) { + hide() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.activity_channel_detail, menu) + + if (!supportsPictureInPictureMode(this)) { + menu.removeItem(R.id.menu_pip) + } + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menu_share -> { + startActivity(Intent.createChooser(currentChannel!!.videoShareIntent, getString(R.string.action_share))) + true + } + R.id.menu_play_in_background -> { + binder!!.movePlaybackToBackground() + finish() + true + } + R.id.menu_pip -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + enterPictureInPictureMode() + } + true + } + android.R.id.home -> { + finish() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + var handled = false + + when (keyCode) { + KeyEvent.KEYCODE_DPAD_LEFT -> handled = prevChannel() + KeyEvent.KEYCODE_DPAD_RIGHT -> handled = nextChannel() + } + + return handled || super.onKeyUp(keyCode, event) + } + + private fun onVideoError(messageResourceId: Int?) { + if (messageResourceId == null || messageResourceId == -1) { + return + } + + player?.pause() + binding.videoProgress.isVisible = false + channelDetailAdapter.currentFragment?.onVideoError(getString(messageResourceId)) + } + + private fun onBufferingChanged(isBuffering: Boolean) { + if (isBuffering) { + binding.videoProgress.isVisible = true + } else if (!player!!.isIdle) { + binding.videoProgress.isVisible = false + channelDetailAdapter.currentFragment?.onVideoStart() + } + } + + override fun onErrorViewClicked() { + player?.recreate() + player?.resume() + } + + private fun onPagerTouch(view: View, motionEvent: MotionEvent): Boolean { + delayHide() + return false + } + + private fun parseIntent(intent: Intent) { + val extras = intent.extras + val channelId = extras!!.getString(EXTRA_CHANNEL_ID) + val channelPosition = channelList.indexOf(channelId) + + binding.viewpager.removeOnPageChangeListener(onPageChangeListener) + binding.viewpager.currentItem = channelPosition + binding.viewpager.addOnPageChangeListener(onPageChangeListener) + } + + private fun pauseActivity() { + disposable.clear() + try { + unbindService(backgroundPlayerServiceConnection) + } catch (ignored: IllegalArgumentException) { + } + } + + private fun resumeActivity() { + binding.programInfo.resume() + bind(this, backgroundPlayerServiceConnection) + } + + private fun playDelayed() { + if (player != null) { + player!!.pause() + } + + playHandler.removeCallbacks(playRunnable) + playHandler.postDelayed(playRunnable, playStreamDelayMillis.toLong()) + } + + private fun play() { + if (currentChannel == null || binder == null) { + return + } + + val currentIntent = getStartIntent(this, currentChannel!!.id) + binder!!.setForegroundActivityIntent(currentIntent) + + Timber.d("play: %s", currentChannel!!.name) + player!!.load(fromChannel(currentChannel!!)) + player!!.resume() + } + + private fun prevChannel(): Boolean { + val nextItemIndex = binding.viewpager.currentItem - 1 + + return if (nextItemIndex < 0) { + false + } else { + delayHide() + binding.viewpager.setCurrentItem(nextItemIndex, true) + true + } + } + + private fun nextChannel(): Boolean { + val nextItemIndex = binding.viewpager.currentItem + 1 + + return if (nextItemIndex == channelDetailAdapter.count) { + false + } else { + delayHide() + binding.viewpager.setCurrentItem(nextItemIndex, true) + true + } + } + + private fun setColor(color: Int) { + binding.videoProgress.indeterminateDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN) + binding.toolbar.setBackgroundColor(color) + + window.statusBarColor = darker(color, 0.075f) + + val colorAlpha = darker(withAlpha(color, 150), 0.25f) + controlsView.setBackgroundColor(colorAlpha) + } +} diff --git a/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/ChannelDetailAdapter.java b/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/ChannelDetailAdapter.java deleted file mode 100644 index 4d28e565..00000000 --- a/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/ChannelDetailAdapter.java +++ /dev/null @@ -1,69 +0,0 @@ -package de.christinecoenen.code.zapp.app.livestream.ui.detail; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentStatePagerAdapter; -import android.view.ViewGroup; - -import de.christinecoenen.code.zapp.models.channels.ChannelModel; -import de.christinecoenen.code.zapp.models.channels.IChannelList; - - -class ChannelDetailAdapter extends FragmentStatePagerAdapter { - - private final IChannelList channelList; - private final Listener listener; - private StreamPageFragment currentFragment; - - ChannelDetailAdapter(FragmentManager fragmentManager, IChannelList channelList, - Listener listener) { - super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); - this.channelList = channelList; - this.listener = listener; - } - - StreamPageFragment getCurrentFragment() { - return currentFragment; - } - - ChannelModel getChannel(int index) { - return channelList.get(index); - } - - @NonNull - @Override - public Fragment getItem(int position) { - ChannelModel channelModel = channelList.get(position); - return StreamPageFragment.newInstance(channelModel); - } - - @Override - public int getCount() { - return channelList.size(); - } - - @Override - public CharSequence getPageTitle(int position) { - return channelList.get(position).getName(); - } - - @Override - public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { - if (currentFragment != object) { - if (currentFragment != null) { - // tell old fragment it's no longer visible - currentFragment.onHide(); - } - - currentFragment = ((StreamPageFragment) object); - listener.onItemSelected(channelList.get(position)); - } - - super.setPrimaryItem(container, position, object); - } - - interface Listener { - void onItemSelected(ChannelModel model); - } -} diff --git a/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/ChannelDetailAdapter.kt b/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/ChannelDetailAdapter.kt new file mode 100644 index 00000000..0f701a55 --- /dev/null +++ b/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/ChannelDetailAdapter.kt @@ -0,0 +1,54 @@ +package de.christinecoenen.code.zapp.app.livestream.ui.detail + +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import de.christinecoenen.code.zapp.models.channels.ChannelModel +import de.christinecoenen.code.zapp.models.channels.IChannelList + +internal class ChannelDetailAdapter( + fragmentManager: FragmentManager, + private val channelList: IChannelList, + private val listener: Listener +) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + var currentFragment: StreamPageFragment? = null + private set + + fun getChannel(index: Int): ChannelModel { + return channelList[index] + } + + override fun getItem(position: Int): Fragment { + val channelModel = channelList[position] + return StreamPageFragment.newInstance(channelModel) + } + + override fun getCount(): Int { + return channelList.size() + } + + override fun getPageTitle(position: Int): CharSequence? { + return channelList[position].name + } + + override fun setPrimaryItem(container: ViewGroup, position: Int, fragmentArgument: Any) { + super.setPrimaryItem(container, position, fragmentArgument) + + if (currentFragment == fragmentArgument) { + return + } + + // tell old fragment it's no longer visible + currentFragment?.onHide() + + currentFragment = fragmentArgument as StreamPageFragment + + listener.onItemSelected(channelList[position]) + } + + interface Listener { + fun onItemSelected(channel: ChannelModel) + } +} diff --git a/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/StreamPageFragment.java b/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/StreamPageFragment.java deleted file mode 100644 index 415b6d0e..00000000 --- a/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/StreamPageFragment.java +++ /dev/null @@ -1,137 +0,0 @@ -package de.christinecoenen.code.zapp.app.livestream.ui.detail; - - -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; - -import de.christinecoenen.code.zapp.databinding.FragmentStreamPageBinding; -import de.christinecoenen.code.zapp.models.channels.ChannelModel; -import timber.log.Timber; - -public class StreamPageFragment extends Fragment { - - private static final String ARGUMENT_CHANNEL_MODEL = "ARGUMENT_CHANNEL_MODEL"; - - private View rootView; - private TextView errorText; - private Listener listener; - - static StreamPageFragment newInstance(ChannelModel channelModel) { - StreamPageFragment fragment = new StreamPageFragment(); - Bundle args = new Bundle(); - args.putSerializable(ARGUMENT_CHANNEL_MODEL, channelModel); - fragment.setArguments(args); - return fragment; - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - FragmentStreamPageBinding binding = FragmentStreamPageBinding.inflate(inflater, container, false); - - rootView = binding.getRoot(); - errorText = binding.textError; - - Bundle args = requireArguments(); - ChannelModel channel = (ChannelModel) args.getSerializable(ARGUMENT_CHANNEL_MODEL); - - if (channel != null) { - ImageView logoView = binding.logo; - logoView.setImageResource(channel.getDrawableId()); - logoView.setContentDescription(channel.getName()); - errorText.setBackgroundColor(channel.getColor()); - - errorText.setOnClickListener(view -> onErrorViewClick()); - - if (channel.getSubtitle() != null) { - TextView subtitleText = binding.subtitle; - subtitleText.setText(channel.getSubtitle()); - } - } else { - Timber.w("channel argument is null"); - } - - return rootView; - } - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - - if (context instanceof Listener) { - listener = (Listener) context; - } else { - throw new RuntimeException("Activity must implement StreamPageFragment.Listener."); - } - } - - @Override - public void onDetach() { - super.onDetach(); - listener = null; - } - - @Override - public void onStop() { - super.onStop(); - // don't use onPause to support multiwindow feature - rootView.setVisibility(View.VISIBLE); - errorText.setVisibility(View.GONE); - } - - void onHide() { - rootView.setVisibility(View.VISIBLE); - errorText.setVisibility(View.GONE); - } - - void onVideoStart() { - fadeOutLogo(); - } - - void onVideoError(String message) { - rootView.setVisibility(View.VISIBLE); - errorText.setVisibility(View.VISIBLE); - errorText.setText(message); - } - - private void onErrorViewClick() { - listener.onErrorViewClicked(); - onHide(); - } - - private void fadeOutLogo() { - if (rootView.getVisibility() == View.VISIBLE) { - Animation fadeOutAnimation = AnimationUtils. - loadAnimation(getContext(), android.R.anim.fade_out); - fadeOutAnimation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - } - - @Override - public void onAnimationEnd(Animation animation) { - rootView.setVisibility(View.GONE); - } - - @Override - public void onAnimationRepeat(Animation animation) { - } - }); - - rootView.startAnimation(fadeOutAnimation); - } - } - - public interface Listener { - void onErrorViewClicked(); - } -} diff --git a/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/StreamPageFragment.kt b/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/StreamPageFragment.kt new file mode 100644 index 00000000..190becc6 --- /dev/null +++ b/app/src/main/java/de/christinecoenen/code/zapp/app/livestream/ui/detail/StreamPageFragment.kt @@ -0,0 +1,122 @@ +package de.christinecoenen.code.zapp.app.livestream.ui.detail + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import de.christinecoenen.code.zapp.databinding.FragmentStreamPageBinding +import de.christinecoenen.code.zapp.models.channels.ChannelModel + +class StreamPageFragment : Fragment() { + + companion object { + private const val ARGUMENT_CHANNEL_MODEL = "ARGUMENT_CHANNEL_MODEL" + + fun newInstance(channelModel: ChannelModel?): StreamPageFragment { + return StreamPageFragment().apply { + arguments = Bundle().apply { + putSerializable(ARGUMENT_CHANNEL_MODEL, channelModel) + } + } + } + } + + private var _binding: FragmentStreamPageBinding? = null + private val binding: FragmentStreamPageBinding get() = _binding!! + + private var listener: Listener? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentStreamPageBinding.inflate(inflater, container, false) + + val channel = requireArguments().getSerializable(ARGUMENT_CHANNEL_MODEL) as ChannelModel? + ?: throw IllegalArgumentException("channel argument is null") + + binding.logo.setImageResource(channel.drawableId) + binding.logo.contentDescription = channel.name + + binding.textError.setBackgroundColor(channel.color) + binding.textError.setOnClickListener { onErrorViewClick() } + + if (channel.subtitle != null) { + binding.subtitle.text = channel.subtitle + } + + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onAttach(context: Context) { + super.onAttach(context) + + listener = if (context is Listener) { + context + } else { + throw RuntimeException("Activity must implement StreamPageFragment.Listener.") + } + } + + override fun onDetach() { + super.onDetach() + listener = null + } + + override fun onStop() { + super.onStop() + + // don't use onPause to support multiwindow feature + binding.root.isVisible = true + binding.textError.isVisible = false + } + + fun onHide() { + binding.root.isVisible = true + binding.textError.isVisible = false + } + + fun onVideoStart() { + fadeOutLogo() + } + + fun onVideoError(message: String?) { + binding.root.isVisible = true + binding.textError.isVisible = true + binding.textError.text = message + } + + private fun onErrorViewClick() { + listener?.onErrorViewClicked() + onHide() + } + + private fun fadeOutLogo() { + if (!binding.root.isVisible) { + return + } + + val fadeOutAnimation = AnimationUtils.loadAnimation(context, android.R.anim.fade_out) + + fadeOutAnimation.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationRepeat(animation: Animation) {} + override fun onAnimationStart(animation: Animation) {} + override fun onAnimationEnd(animation: Animation) { + binding.root.isVisible = false + } + }) + + binding.root.startAnimation(fadeOutAnimation) + } + + interface Listener { + fun onErrorViewClicked() + } +}