• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.dashboard.suggestions
18 
19 import android.app.ActivityOptions
20 import android.app.PendingIntent
21 import android.app.settings.SettingsEnums
22 import android.content.Context
23 import android.os.Bundle
24 import android.os.SystemClock
25 import android.service.settings.suggestions.Suggestion
26 import android.util.Log
27 import android.view.LayoutInflater
28 import android.view.View
29 import android.view.ViewGroup
30 import android.widget.ImageView
31 import android.widget.TextView
32 import com.android.settings.core.InstrumentedFragment
33 import com.android.settings.homepage.SettingsHomepageActivity
34 import com.android.settings.homepage.SplitLayoutListener
35 import com.android.settings.overlay.FeatureFactory
36 import com.android.settings.R
37 import com.android.settingslib.suggestions.SuggestionController
38 import kotlinx.coroutines.CoroutineScope
39 import kotlinx.coroutines.Dispatchers
40 import kotlinx.coroutines.Job
41 import kotlinx.coroutines.launch
42 import kotlinx.coroutines.withContext
43 
44 private const val SUGGESTIONS = "suggestions"
45 private const val TAG = "ContextualSuggestFrag"
46 private const val FLAG_IS_DISMISSIBLE = 1 shl 2
47 
48 /**
49  * Fragment to control display and interaction logic for [Suggestion]s
50  */
51 class SuggestionFragment : InstrumentedFragment(),
52     SplitLayoutListener, SuggestionController.ServiceConnectionListener {
53 
54     private val scope = CoroutineScope(Job() + Dispatchers.Main)
55     private lateinit var suggestionController: SuggestionController
56     private lateinit var suggestionTile: View
57     private var icon: ImageView? = null
58     private var iconFrame: View? = null
59     private var title: TextView? = null
60     private var summary: TextView? = null
61     private var dismiss: ImageView? = null
62     private var iconVisible = true
63     private var startTime: Long = 0
64     private var suggestionsRestored = false
65     private var splitLayoutSupported = false
66 
67     override fun onAttach(context: Context) {
68         super.onAttach(context)
69         val component = FeatureFactory.featureFactory
70             .suggestionFeatureProvider
71             .suggestionServiceComponent
72         suggestionController = SuggestionController(context, component, this)
73     }
74 
75     override fun onCreateView(
76         inflater: LayoutInflater,
77         container: ViewGroup?,
78         savedInstanceState: Bundle?
79     ): View? {
80         suggestionTile = inflater.inflate(R.layout.suggestion_tile, container, true)
81         icon = suggestionTile.findViewById(android.R.id.icon)
82         iconFrame = suggestionTile.findViewById(android.R.id.icon_frame)
83         title = suggestionTile.findViewById(android.R.id.title)
84         summary = suggestionTile.findViewById(android.R.id.summary)
85         dismiss = suggestionTile.findViewById(android.R.id.closeButton)
86         if (!iconVisible) {
87             onSplitLayoutChanged(false)
88         }
89         // Restore the suggestion and skip reloading
90         if (savedInstanceState != null) {
91             Log.d(TAG, "Restoring suggestions")
92             savedInstanceState.getParcelableArrayList(
93                 SUGGESTIONS,
94                 Suggestion::class.java
95             )?.let { suggestions ->
96                 suggestionsRestored = true
97                 startTime = SystemClock.uptimeMillis()
98                 updateState(suggestions)
99             }
100         }
101 
102         return super.onCreateView(inflater, container, savedInstanceState)
103     }
104 
105     override fun onSaveInstanceState(outState: Bundle) {
106         outState.putParcelableArrayList(SUGGESTIONS, currentSuggestions)
107         super.onSaveInstanceState(outState)
108     }
109 
110     override fun onStart() {
111         super.onStart()
112         suggestionController.start()
113     }
114 
115     override fun onStop() {
116         suggestionController.stop()
117         super.onStop()
118     }
119 
120     override fun getMetricsCategory(): Int {
121         return SettingsEnums.SETTINGS_HOMEPAGE
122     }
123 
124     override fun setSplitLayoutSupported(supported: Boolean) {
125         splitLayoutSupported = supported
126     }
127 
128     override fun onSplitLayoutChanged(isRegularLayout: Boolean) {
129         iconVisible = isRegularLayout
130         if (splitLayoutSupported) {
131             iconFrame?.visibility = if (iconVisible) View.VISIBLE else View.GONE
132         }
133     }
134 
135     override fun onServiceConnected() {
136         loadSuggestions()
137     }
138 
139     override fun onServiceDisconnected() {
140         // no-op
141     }
142 
143     private fun loadSuggestions() {
144         if (suggestionsRestored) {
145             // Skip first suggestion loading when restored
146             suggestionsRestored = false
147             return
148         }
149 
150         startTime = SystemClock.uptimeMillis()
151         scope.launch(Dispatchers.IO) {
152             Log.d(TAG, "Start loading suggestions")
153             val suggestions = suggestionController.suggestions
154             Log.d(TAG, "Loaded suggestions: ${suggestions?.size}")
155             withContext(Dispatchers.Main) {
156                 updateState(suggestions)
157             }
158         }
159     }
160 
161     private fun updateState(suggestions: List<Suggestion>?) {
162         currentSuggestions.clear()
163         if (suggestions.isNullOrEmpty()) {
164             Log.d(TAG, "Remove suggestions")
165             showSuggestionTile(false)
166             return
167         }
168         currentSuggestions.addAll(suggestions)
169 
170         // Only take top suggestion; we assume this is the highest rank.
171         val suggestion = suggestions.first()
172         icon?.setImageIcon(suggestion.icon)
173         suggestion.title?.let {
174             title?.text = it
175         } ?: run {
176             Log.d(TAG, "No suggestion title, removing")
177             showSuggestionTile(false)
178             return
179         }
180         val suggestionSummary = suggestion.summary
181         if (suggestionSummary.isNullOrEmpty()) {
182             summary?.visibility = View.GONE
183         } else {
184             summary?.visibility = View.VISIBLE
185             summary?.text = suggestionSummary
186         }
187         if (suggestion.flags and FLAG_IS_DISMISSIBLE != 0) {
188             dismiss?.let { dismissView ->
189                 dismissView.visibility = View.VISIBLE
190                 dismissView.setOnClickListener {
191                     scope.launch(Dispatchers.IO) {
192                         suggestionController.dismissSuggestions(suggestion)
193                     }
194                     if (suggestions.size > 1) {
195                         dismissView.visibility = View.GONE
196                         updateState(suggestions.subList(1, suggestions.size))
197                     } else {
198                         currentSuggestions.clear()
199                         suggestionTile.visibility = View.GONE
200                     }
201                 }
202             }
203         }
204         suggestionTile.setOnClickListener {
205             // Notify service that suggestion is being launched. Note that the service does not
206             // actually start the suggestion on our behalf, instead simply logging metrics.
207             scope.launch(Dispatchers.IO) {
208                 suggestionController.launchSuggestion(suggestion)
209             }
210             currentSuggestions.clear()
211             try {
212                 val options = ActivityOptions.makeBasic()
213                     .setPendingIntentBackgroundActivityStartMode(
214                         ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
215                     )
216                 suggestion.pendingIntent.send(options.toBundle())
217             } catch (e: PendingIntent.CanceledException) {
218                 Log.e(TAG, "Failed to start suggestion ${suggestion.title}", e)
219             }
220         }
221         showSuggestionTile(true)
222     }
223 
224     private fun showSuggestionTile(show: Boolean) {
225         val totalTime = SystemClock.uptimeMillis() - startTime
226         Log.d(TAG, "Total loading time: $totalTime ms")
227         mMetricsFeatureProvider.action(
228             context,
229             SettingsEnums.ACTION_CONTEXTUAL_HOME_SHOW,
230             totalTime.toInt()
231         )
232         (activity as? SettingsHomepageActivity)?.showHomepageWithSuggestion(show)
233     }
234 
235     private companion object {
236         val currentSuggestions = arrayListOf<Suggestion>()
237     }
238 }