/* * Copyright (C) 2022-2023 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package foundation.e.advancedprivacy.features.trackers import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle import android.text.Spannable import android.text.SpannableString import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.text.style.UnderlineSpan import android.view.View import android.view.ViewTreeObserver import android.widget.Toast import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayoutMediator import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.GraphHolder import foundation.e.advancedprivacy.common.NavToolbarFragment import foundation.e.advancedprivacy.common.extensions.findViewHolderForAdapterPosition import foundation.e.advancedprivacy.common.extensions.updatePagerHeightForChild import foundation.e.advancedprivacy.databinding.FragmentTrackersBinding import foundation.e.advancedprivacy.databinding.TrackersItemGraphBinding import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { private val viewModel: TrackersViewModel by viewModel() private lateinit var binding: FragmentTrackersBinding private var dayGraphHolder: GraphHolder? = null private var monthGraphHolder: GraphHolder? = null private var yearGraphHolder: GraphHolder? = null private lateinit var tabAdapter: ListsTabPagerAdapter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentTrackersBinding.bind(view) dayGraphHolder = GraphHolder(binding.graphDay.graph, requireContext(), false) monthGraphHolder = GraphHolder(binding.graphMonth.graph, requireContext(), false) yearGraphHolder = GraphHolder(binding.graphYear.graph, requireContext(), false) tabAdapter = ListsTabPagerAdapter(requireContext(), viewModel) binding.listsPager.adapter = tabAdapter TabLayoutMediator(binding.listsTabs, binding.listsPager) { tab, position -> tab.text = getString( when (position) { TAB_APPS -> R.string.trackers_toggle_list_apps else -> R.string.trackers_toggle_list_trackers } ) }.attach() binding.listsPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageScrollStateChanged(state: Int) { super.onPageScrollStateChanged(state) if (state == ViewPager2.SCROLL_STATE_IDLE) { updatePagerHeight() } } }) setupTrackersInfos() listenViewModel() } private fun listenViewModel() { with(viewLifecycleOwner) { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { render(viewModel.state.value) viewModel.state.collect(::render) } } lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.singleEvents.collect(::handleEvents) } } lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.navigate.collect(findNavController()::navigate) } } lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.doOnStartedState() } } } } private fun handleEvents(event: TrackersViewModel.SingleEvent) { when (event) { is TrackersViewModel.SingleEvent.ErrorEvent -> { displayToast(event.error) } is TrackersViewModel.SingleEvent.OpenUrl -> { try { startActivity(Intent(Intent.ACTION_VIEW, event.url)) } catch (e: ActivityNotFoundException) { Toast.makeText( requireContext(), R.string.error_no_activity_view_url, Toast.LENGTH_SHORT ).show() } } } } private fun setupTrackersInfos() { val infoText = getString(R.string.trackers_info) val moreText = getString(R.string.trackers_info_more) val spannable = SpannableString("$infoText $moreText") val startIndex = infoText.length + 1 val endIndex = spannable.length spannable.setSpan( ForegroundColorSpan(ContextCompat.getColor(requireContext(), R.color.accent)), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) spannable.setSpan(UnderlineSpan(), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) spannable.setSpan( object : ClickableSpan() { override fun onClick(p0: View) { viewModel.onClickLearnMore() } }, startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) with(binding.trackersInfo) { linksClickable = true isClickable = true movementMethod = LinkMovementMethod.getInstance() text = spannable } } private var oldPosition = -1 private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { binding.listsPager.findViewHolderForAdapterPosition(binding.listsPager.currentItem) .let { currentViewHolder -> currentViewHolder?.itemView?.let { binding.listsPager.updatePagerHeightForChild(it) } } } private fun updatePagerHeight() { with(binding.listsPager) { val position = currentItem if (position == oldPosition) return if (oldPosition > 0) { val oldItem = findViewHolderForAdapterPosition(oldPosition)?.itemView oldItem?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener) } val newItem = findViewHolderForAdapterPosition(position)?.itemView newItem?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener) oldPosition = position adapter?.notifyItemChanged(position) } } private fun displayToast(message: String) { Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } private fun render(state: TrackersState) { state.dayStatistics?.let { renderGraph(it, dayGraphHolder!!, binding.graphDay) } state.monthStatistics?.let { renderGraph(it, monthGraphHolder!!, binding.graphMonth) } state.yearStatistics?.let { renderGraph(it, yearGraphHolder!!, binding.graphYear) } updatePagerHeight() tabAdapter.updateDataSet(state.apps, state.trackers) } private fun renderGraph( statistics: TrackersPeriodicStatistics, graphHolder: GraphHolder, graphBinding: TrackersItemGraphBinding ) { if (statistics.callsBlockedNLeaked.all { it.first == 0 && it.second == 0 }) { graphBinding.graph.visibility = View.INVISIBLE graphBinding.graphEmpty.isVisible = true } else { graphBinding.graph.isVisible = true graphBinding.graphEmpty.isVisible = false graphHolder.data = statistics.callsBlockedNLeaked graphHolder.labels = statistics.periods graphBinding.trackersCountLabel.text = getString(R.string.trackers_count_label, statistics.trackersCount) } } override fun onDestroyView() { super.onDestroyView() kotlin.runCatching { if (oldPosition >= 0) { val oldItem = binding.listsPager.findViewHolderForAdapterPosition(oldPosition) oldItem?.itemView?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener) } } dayGraphHolder = null monthGraphHolder = null yearGraphHolder = null } }