/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.biometrics2.ui.view import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRINT_UNLOCK_DISABLED import android.content.Context import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter import android.os.Bundle import android.text.Html import android.text.method.LinkMovementMethod import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.ScrollView import android.widget.TextView import androidx.annotation.StringRes import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.settings.R import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus import com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX import com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK import com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_UNKNOWN import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel import com.google.android.setupcompat.template.FooterBarMixin import com.google.android.setupcompat.template.FooterButton import com.google.android.setupdesign.GlifLayout import com.google.android.setupdesign.template.RequireScrollMixin import com.google.android.setupdesign.util.DeviceHelper import com.google.android.setupdesign.util.DynamicColorPalette import com.google.android.setupdesign.util.DynamicColorPalette.ColorType.ACCENT import java.util.function.Supplier import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch /** * Fingerprint intro onboarding page fragment implementation */ class FingerprintEnrollIntroFragment : Fragment() { private val viewModelProvider: ViewModelProvider get() = ViewModelProvider(requireActivity()) private var _viewModel: FingerprintEnrollIntroViewModel? = null private val viewModel: FingerprintEnrollIntroViewModel get() = _viewModel!! private var introView: GlifLayout? = null private var primaryFooterButton: FooterButton? = null private var secondaryFooterButton: FooterButton? = null private val onNextClickListener = View.OnClickListener { _: View? -> activity?.lifecycleScope?.let { viewModel.onNextButtonClick(it) } } private val onSkipOrCancelClickListener = View.OnClickListener { _: View? -> activity?.lifecycleScope?.let { viewModel.onSkipOrCancelButtonClick(it) } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { introView = inflater.inflate( R.layout.fingerprint_enroll_introduction, container, false ) as GlifLayout return introView!! } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) requireActivity().bindFingerprintEnrollIntroView( view = introView!!, canAssumeUdfps = viewModel.canAssumeUdfps, isBiometricUnlockDisabledByAdmin = viewModel.isBiometricUnlockDisabledByAdmin, isParentalConsentRequired = viewModel.isParentalConsentRequired, descriptionDisabledByAdminSupplier = { getDescriptionDisabledByAdmin(view.context) } ) } override fun onStart() { val context: Context = requireContext() val footerBarMixin: FooterBarMixin = footerBarMixin viewModel.updateEnrollableStatus(lifecycleScope) initPrimaryFooterButton(context, footerBarMixin) initSecondaryFooterButton(context, footerBarMixin) collectPageStatusFlowIfNeed() super.onStart() } private fun initPrimaryFooterButton( context: Context, footerBarMixin: FooterBarMixin ) { if (footerBarMixin.primaryButton != null) { return } primaryFooterButton = FooterButton.Builder(context) .setText(R.string.security_settings_fingerprint_enroll_introduction_agree) .setButtonType(FooterButton.ButtonType.OPT_IN) .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary) .build() .also { it.setOnClickListener(onNextClickListener) footerBarMixin.primaryButton = it } } private fun initSecondaryFooterButton( context: Context, footerBarMixin: FooterBarMixin ) { if (footerBarMixin.secondaryButton != null) { return } secondaryFooterButton = FooterButton.Builder(context) .setText( if (viewModel.request.isAfterSuwOrSuwSuggestedAction) R.string.security_settings_fingerprint_enroll_introduction_cancel else R.string.security_settings_fingerprint_enroll_introduction_no_thanks ) .setButtonType(FooterButton.ButtonType.NEXT) .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary) .build() .also { it.setOnClickListener(onSkipOrCancelClickListener) footerBarMixin.setSecondaryButton(it, true /* usePrimaryStyle */) } } private fun collectPageStatusFlowIfNeed() { lifecycleScope.launch { val status = viewModel.pageStatusFlow.first() Log.d(TAG, "collectPageStatusFlowIfNeed status:$status") if (status.hasScrollToBottom() || status.enrollableStatus === FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX ) { // Update once and do not requireScrollWithButton() again when page has // scrolled to bottom or User has enrolled at least a fingerprint, because if // we requireScrollWithButton() again, primary button will become "More" after // scrolling. updateFooterButtons(status) } else { introView!!.getMixin(RequireScrollMixin::class.java).let { it.requireScrollWithButton( requireActivity(), primaryFooterButton!!, moreButtonTextRes, onNextClickListener ) it.setOnRequireScrollStateChangedListener { scrollNeeded: Boolean -> viewModel.setHasScrolledToBottom(!scrollNeeded, lifecycleScope) } } repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.pageStatusFlow.collect( this@FingerprintEnrollIntroFragment::updateFooterButtons ) } } } } override fun onAttach(context: Context) { _viewModel = viewModelProvider[FingerprintEnrollIntroViewModel::class.java] super.onAttach(context) } private val footerBarMixin: FooterBarMixin get() = introView!!.getMixin(FooterBarMixin::class.java) private fun getDescriptionDisabledByAdmin(context: Context): String? { val defaultStrId: Int = R.string.security_settings_fingerprint_enroll_introduction_message_unlock_disabled val devicePolicyManager: DevicePolicyManager = checkNotNull(requireActivity().getSystemService(DevicePolicyManager::class.java)) return devicePolicyManager.resources.getString(FINGERPRINT_UNLOCK_DISABLED) { context.getString(defaultStrId) } } private fun updateFooterButtons(status: FingerprintEnrollIntroStatus) { if (DEBUG) { Log.d(TAG, "updateFooterButtons($status)") } primaryFooterButton!!.setText( context, if (status.enrollableStatus === FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX) R.string.done else if (status.hasScrollToBottom()) R.string.security_settings_fingerprint_enroll_introduction_agree else moreButtonTextRes ) secondaryFooterButton!!.visibility = if (status.hasScrollToBottom() && status.enrollableStatus !== FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX ) View.VISIBLE else View.INVISIBLE view!!.requireViewById(R.id.error_text).let { when (status.enrollableStatus) { FINGERPRINT_ENROLLABLE_OK -> { it.text = null it.visibility = View.GONE } FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX -> { it.setText(R.string.fingerprint_intro_error_max) it.visibility = View.VISIBLE } FINGERPRINT_ENROLLABLE_UNKNOWN -> {} } } } @get:StringRes private val moreButtonTextRes: Int get() = R.string.security_settings_face_enroll_introduction_more companion object { private const val TAG = "FingerprintEnrollIntroFragment" private const val DEBUG = false } } fun FragmentActivity.bindFingerprintEnrollIntroView( view: GlifLayout, canAssumeUdfps: Boolean, isBiometricUnlockDisabledByAdmin: Boolean, isParentalConsentRequired: Boolean, descriptionDisabledByAdminSupplier: Supplier ) { val context = view.context val iconFingerprint = view.findViewById(R.id.icon_fingerprint)!! val iconDeviceLocked = view.findViewById(R.id.icon_device_locked)!! val iconTrashCan = view.findViewById(R.id.icon_trash_can)!! val iconInfo = view.findViewById(R.id.icon_info)!! val iconShield = view.findViewById(R.id.icon_shield)!! val iconLink = view.findViewById(R.id.icon_link)!! val footerMessage6 = view.findViewById(R.id.footer_message_6)!! PorterDuffColorFilter( DynamicColorPalette.getColor(context, ACCENT), PorterDuff.Mode.SRC_IN ).let { colorFilter -> iconFingerprint.drawable.colorFilter = colorFilter iconDeviceLocked.drawable.colorFilter = colorFilter iconTrashCan.drawable.colorFilter = colorFilter iconInfo.drawable.colorFilter = colorFilter iconShield.drawable.colorFilter = colorFilter iconLink.drawable.colorFilter = colorFilter } view.findViewById(R.id.footer_learn_more)!!.let { learnMore -> learnMore.movementMethod = LinkMovementMethod.getInstance() val footerLinkStr: String = context.getString( R.string.security_settings_fingerprint_v2_enroll_introduction_message_learn_more, Html.FROM_HTML_MODE_LEGACY ) learnMore.text = Html.fromHtml(footerLinkStr) } if (canAssumeUdfps) { footerMessage6.visibility = View.VISIBLE iconShield.visibility = View.VISIBLE } else { footerMessage6.visibility = View.GONE iconShield.visibility = View.GONE } val glifLayoutHelper = GlifLayoutHelper(this, view) if (isBiometricUnlockDisabledByAdmin && !isParentalConsentRequired) { glifLayoutHelper.setHeaderText( R.string.security_settings_fingerprint_enroll_introduction_title_unlock_disabled ) glifLayoutHelper.setDescriptionText(descriptionDisabledByAdminSupplier.get()) } else { glifLayoutHelper.setHeaderText( R.string.security_settings_fingerprint_enroll_introduction_title ) glifLayoutHelper.setDescriptionText( getString( R.string.security_settings_fingerprint_enroll_introduction_v3_message, DeviceHelper.getDeviceName(context) ) ) } view.findViewById(com.google.android.setupdesign.R.id.sud_scroll_view) ?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES }