/* * Copyright (C) 2025 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.healthconnect.controller.shared.dialog import android.app.Dialog import android.content.Context import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import com.android.healthconnect.controller.R import com.android.healthconnect.controller.utils.logging.HealthConnectLogger import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject /** * A [BottomSheetDialogFragment] that displays a fragment as a bottom sheet. * * This fragment is responsible for: * - Hosting a content fragment provided via [newInstance]. * - Notifying an [OnCancelListener] when the dialog is canceled. */ @AndroidEntryPoint(BottomSheetDialogFragment::class) class HealthConnectBottomSheetDialogFragment : Hilt_HealthConnectBottomSheetDialogFragment() { @Inject lateinit var logger: HealthConnectLogger private var contentFragment: Fragment? = null private var cancelListener: OnCancelListener? = null /** * Listener interface for when the dialog is canceled. Activities or fragments hosting this * dialog should implement this interface. */ interface OnCancelListener { fun onDialogCanceled() } companion object { private const val FRAGMENT_CLASS_KEY = "bottom_sheet_fragment_class" /** * Creates a new instance of [HealthConnectBottomSheetDialogFragment]. * * @param fragmentClass The class of the [Fragment] to be displayed inside the bottom sheet. */ fun newInstance( fragmentClass: Class ): HealthConnectBottomSheetDialogFragment { return HealthConnectBottomSheetDialogFragment().apply { arguments = Bundle().apply { putSerializable(FRAGMENT_CLASS_KEY, fragmentClass) } } } } override fun onAttach(context: Context) { super.onAttach(context) cancelListener = try { context as OnCancelListener } catch (e: ClassCastException) { throw ClassCastException("$context must implement OnCancelListener") } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val fragmentClass = arguments?.getSerializable(FRAGMENT_CLASS_KEY) as? Class if (fragmentClass != null) { contentFragment = fragmentClass.getDeclaredConstructor().newInstance() } else { dismiss() } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View? { val view = inflater.inflate(R.layout.health_connect_bottom_sheet, container, false) childFragmentManager .beginTransaction() .replace(R.id.bottom_sheet_fragment_container, contentFragment ?: Fragment()) .commit() return view } /** * Replaces the currently displayed content fragment with a new one. * * @param fragment The new [Fragment] to display. */ fun replaceFragment(fragment: Fragment) { if (contentFragment?.javaClass == fragment.javaClass) { return } contentFragment = fragment childFragmentManager .beginTransaction() .replace(R.id.bottom_sheet_fragment_container, fragment) .commit() } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog dialog.setOnShowListener { val bottomSheet = dialog.findViewById( com.google.android.material.R.id.design_bottom_sheet ) bottomSheet?.let { frameLayout -> frameLayout.background = ContextCompat.getDrawable(requireContext(), R.drawable.rounded_background) val behavior = BottomSheetBehavior.from(frameLayout) behavior.state = STATE_EXPANDED behavior.isFitToContents = false behavior.expandedOffset = 0 } } return dialog } override fun onCancel(dialog: DialogInterface) { super.onCancel(dialog) cancelListener?.onDialogCanceled() } }