1 /**
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * ```
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  * ```
10  *
11  * Unless required by applicable law or agreed to in writing, software distributed under the License
12  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13  * or implied. See the License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 package com.android.healthconnect.controller.permissions.connectedapps.searchapps
17 
18 import android.content.Intent.EXTRA_PACKAGE_NAME
19 import android.os.Bundle
20 import android.view.LayoutInflater
21 import android.view.Menu
22 import android.view.MenuInflater
23 import android.view.MenuItem
24 import android.view.View
25 import android.view.ViewGroup
26 import android.widget.SearchView
27 import androidx.core.os.bundleOf
28 import androidx.core.view.MenuHost
29 import androidx.core.view.MenuProvider
30 import androidx.fragment.app.viewModels
31 import androidx.lifecycle.Lifecycle
32 import androidx.navigation.fragment.findNavController
33 import androidx.preference.Preference
34 import androidx.preference.PreferenceFragmentCompat
35 import androidx.preference.PreferenceGroup
36 import com.android.healthconnect.controller.R
37 import com.android.healthconnect.controller.permissions.connectedapps.ConnectedAppsViewModel
38 import com.android.healthconnect.controller.permissions.connectedapps.HealthAppPreference
39 import com.android.healthconnect.controller.shared.Constants.EXTRA_APP_NAME
40 import com.android.healthconnect.controller.shared.app.ConnectedAppMetadata
41 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.ALLOWED
42 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.DENIED
43 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.INACTIVE
44 import com.android.healthconnect.controller.utils.logging.AppPermissionsElement
45 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
46 import com.android.healthconnect.controller.utils.logging.PageName
47 import com.android.settingslib.widget.TopIntroPreference
48 import com.google.android.material.appbar.AppBarLayout
49 import dagger.hilt.android.AndroidEntryPoint
50 import javax.inject.Inject
51 
52 /** Fragment for search apps screen. */
53 @AndroidEntryPoint(PreferenceFragmentCompat::class)
54 class SearchAppsFragment : Hilt_SearchAppsFragment() {
55 
56     companion object {
57         const val ALLOWED_APPS_CATEGORY = "allowed_apps"
58         private const val NOT_ALLOWED_APPS = "not_allowed_apps"
59         private const val INACTIVE_APPS = "inactive_apps"
60         private const val EMPTY_SEARCH_RESULT = "no_search_result_preference"
61         private const val TOP_INTRO_PREF = "search_apps_top_intro"
62     }
63 
64     @Inject lateinit var logger: HealthConnectLogger
65     private val pageName = PageName.SEARCH_APPS_PAGE
66 
67     private var searchView: SearchView? = null
68     private val viewModel: ConnectedAppsViewModel by viewModels()
69 
70     private val allowedAppsCategory: PreferenceGroup by lazy {
71         preferenceScreen.findPreference(ALLOWED_APPS_CATEGORY)!!
72     }
73     private val notAllowedAppsCategory: PreferenceGroup by lazy {
74         preferenceScreen.findPreference(NOT_ALLOWED_APPS)!!
75     }
76     private val inactiveAppsPreference: PreferenceGroup by lazy {
77         preferenceScreen.findPreference(INACTIVE_APPS)!!
78     }
79     private val emptySearchResultsPreference: NoSearchResultPreference by lazy {
80         preferenceScreen.findPreference(EMPTY_SEARCH_RESULT)!!
81     }
82     private val topIntroPreference: TopIntroPreference by lazy {
83         preferenceScreen.findPreference(TOP_INTRO_PREF)!!
84     }
85 
86     private val menuProvider =
87         object : MenuProvider, SearchView.OnQueryTextListener {
88             override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
89                 menuInflater.inflate(R.menu.search_apps, menu)
90                 val searchMenuItem = menu.findItem(R.id.menu_search_apps)
91                 searchMenuItem.expandActionView()
92                 searchMenuItem.setOnActionExpandListener(
93                     object : MenuItem.OnActionExpandListener {
94 
95                         override fun onMenuItemActionExpand(item: MenuItem): Boolean {
96                             return true
97                         }
98 
99                         override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
100                             showTitleFromCollapsingToolbarLayout()
101                             findNavController().popBackStack()
102                             return true
103                         }
104                     })
105                 searchView = searchMenuItem.actionView as SearchView
106                 searchView!!.queryHint = getText(R.string.search_connected_apps)
107                 searchView!!.setOnQueryTextListener(this)
108                 searchView!!.maxWidth = Int.MAX_VALUE
109             }
110 
111             override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
112                 return false
113             }
114 
115             override fun onQueryTextSubmit(p0: String?): Boolean {
116                 return false
117             }
118 
119             override fun onQueryTextChange(newText: String): Boolean {
120                 viewModel.searchConnectedApps(newText)
121                 return true
122             }
123         }
124 
125     override fun onCreate(savedInstanceState: Bundle?) {
126         super.onCreate(savedInstanceState)
127         logger.setPageId(pageName)
128     }
129 
130     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
131         setPreferencesFromResource(R.xml.search_apps_screen, rootKey)
132         preferenceScreen.addPreference(NoSearchResultPreference(requireContext()))
133     }
134 
135     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
136         super.onViewCreated(view, savedInstanceState)
137         setupMenu()
138         hideTitleFromCollapsingToolbarLayout()
139         viewModel.connectedApps.observe(viewLifecycleOwner) { connectedApps ->
140             emptySearchResultsPreference?.isVisible = connectedApps.isEmpty()
141             emptySearchResultsPreference?.isSelectable = false
142             topIntroPreference?.isVisible = connectedApps.isNotEmpty()
143 
144             val connectedAppsGroup = connectedApps.groupBy { it.status }
145             updateAllowedApps(connectedAppsGroup[ALLOWED].orEmpty())
146             updateDeniedApps(connectedAppsGroup[DENIED].orEmpty())
147             updateInactiveApps(connectedAppsGroup[INACTIVE].orEmpty())
148         }
149     }
150 
151     override fun onDestroyView() {
152         super.onDestroyView()
153         showTitleFromCollapsingToolbarLayout()
154     }
155 
156     override fun onResume() {
157         super.onResume()
158         hideTitleFromCollapsingToolbarLayout()
159         logger.setPageId(pageName)
160         logger.logPageImpression()
161     }
162 
163     override fun onCreateView(
164         inflater: LayoutInflater,
165         container: ViewGroup?,
166         savedInstanceState: Bundle?
167     ): View {
168         logger.setPageId(pageName)
169         return super.onCreateView(inflater, container, savedInstanceState)
170     }
171 
172     private fun updateInactiveApps(appsList: List<ConnectedAppMetadata>) {
173         inactiveAppsPreference?.removeAll()
174         if (appsList.isEmpty()) {
175             preferenceScreen.removePreference(inactiveAppsPreference)
176         } else {
177             preferenceScreen.addPreference(inactiveAppsPreference)
178             appsList.forEach { app -> inactiveAppsPreference?.addPreference(getAppPreference(app)) }
179         }
180     }
181 
182     private fun updateAllowedApps(appsList: List<ConnectedAppMetadata>) {
183         allowedAppsCategory?.removeAll()
184         if (appsList.isEmpty()) {
185             preferenceScreen.removePreference(allowedAppsCategory)
186         } else {
187             preferenceScreen.addPreference(allowedAppsCategory)
188             appsList.forEach { app ->
189                 allowedAppsCategory?.addPreference(
190                     getAppPreference(app) { navigateToAppInfoScreen(app) })
191             }
192         }
193     }
194 
195     private fun updateDeniedApps(appsList: List<ConnectedAppMetadata>) {
196         notAllowedAppsCategory?.removeAll()
197         if (appsList.isEmpty()) {
198             preferenceScreen.removePreference(notAllowedAppsCategory)
199         } else {
200             preferenceScreen.addPreference(notAllowedAppsCategory)
201             appsList.forEach { app ->
202                 notAllowedAppsCategory?.addPreference(
203                     getAppPreference(app) { navigateToAppInfoScreen(app) })
204             }
205         }
206     }
207 
208     private fun navigateToAppInfoScreen(app: ConnectedAppMetadata) {
209         findNavController()
210             .navigate(
211                 R.id.action_searchApps_to_connectedApp,
212                 bundleOf(
213                     EXTRA_PACKAGE_NAME to app.appMetadata.packageName,
214                     EXTRA_APP_NAME to app.appMetadata.appName))
215     }
216 
217     private fun getAppPreference(
218         app: ConnectedAppMetadata,
219         onClick: (() -> Unit)? = null
220     ): Preference {
221         return HealthAppPreference(requireContext(), app.appMetadata).also {
222             if (app.status == ALLOWED) {
223                 it.logName = AppPermissionsElement.CONNECTED_APP_BUTTON
224             } else if (app.status == DENIED) {
225                 it.logName = AppPermissionsElement.NOT_CONNECTED_APP_BUTTON
226             } else if (app.status == INACTIVE) {
227                 it.logName = AppPermissionsElement.INACTIVE_APP_BUTTON
228             }
229             it.setOnPreferenceClickListener {
230                 onClick?.invoke()
231                 true
232             }
233         }
234     }
235 
236     private fun setupMenu() {
237         (activity as MenuHost).addMenuProvider(
238             menuProvider, viewLifecycleOwner, Lifecycle.State.RESUMED)
239     }
240 
241     private fun hideTitleFromCollapsingToolbarLayout() {
242         activity?.findViewById<AppBarLayout>(
243             com.android.settingslib.collapsingtoolbar.R.id.app_bar)?.setExpanded(false)
244     }
245 
246     private fun showTitleFromCollapsingToolbarLayout() {
247         activity?.findViewById<AppBarLayout>(
248             com.android.settingslib.collapsingtoolbar.R.id.app_bar)?.setExpanded(true)
249     }
250 }
251