1 /* 2 * Copyright (C) 2020 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.systemui.controls.management 18 19 import android.app.ActivityOptions 20 import android.content.ComponentName 21 import android.content.Context 22 import android.content.Intent 23 import android.os.Bundle 24 import android.util.Log 25 import android.view.View 26 import android.view.ViewGroup 27 import android.view.ViewStub 28 import android.widget.Button 29 import android.widget.TextView 30 import android.widget.Toast 31 import android.window.OnBackInvokedCallback 32 import android.window.OnBackInvokedDispatcher 33 import androidx.activity.ComponentActivity 34 import androidx.recyclerview.widget.GridLayoutManager 35 import androidx.recyclerview.widget.ItemTouchHelper 36 import androidx.recyclerview.widget.RecyclerView 37 import com.android.systemui.res.R 38 import com.android.systemui.controls.CustomIconCache 39 import com.android.systemui.controls.controller.ControlsControllerImpl 40 import com.android.systemui.controls.controller.StructureInfo 41 import com.android.systemui.controls.ui.ControlsActivity 42 import com.android.systemui.dagger.qualifiers.Main 43 import com.android.systemui.settings.UserTracker 44 import java.util.concurrent.Executor 45 import javax.inject.Inject 46 47 /** 48 * Activity for rearranging and removing controls for a given structure 49 */ 50 open class ControlsEditingActivity @Inject constructor( 51 @Main private val mainExecutor: Executor, 52 private val controller: ControlsControllerImpl, 53 private val userTracker: UserTracker, 54 private val customIconCache: CustomIconCache, 55 ) : ComponentActivity() { 56 57 companion object { 58 private const val DEBUG = false 59 private const val TAG = "ControlsEditingActivity" 60 const val EXTRA_STRUCTURE = ControlsFavoritingActivity.EXTRA_STRUCTURE 61 const val EXTRA_APP = ControlsFavoritingActivity.EXTRA_APP 62 const val EXTRA_FROM_FAVORITING = "extra_from_favoriting" 63 private val SUBTITLE_ID = R.string.controls_favorite_rearrange 64 private val EMPTY_TEXT_ID = R.string.controls_favorite_removed 65 } 66 67 private lateinit var component: ComponentName 68 private lateinit var structure: CharSequence 69 private lateinit var model: FavoritesModel 70 private lateinit var subtitle: TextView 71 private lateinit var saveButton: View 72 private lateinit var addControls: View 73 74 private var isFromFavoriting: Boolean = false 75 76 private val userTrackerCallback: UserTracker.Callback = object : UserTracker.Callback { 77 private val startingUser = controller.currentUserId 78 onUserChangednull79 override fun onUserChanged(newUser: Int, userContext: Context) { 80 if (newUser != startingUser) { 81 userTracker.removeCallback(this) 82 finish() 83 } 84 } 85 } 86 <lambda>null87 private val mOnBackInvokedCallback = OnBackInvokedCallback { 88 if (DEBUG) { 89 Log.d(TAG, "Predictive Back dispatcher called mOnBackInvokedCallback") 90 } 91 onBackPressed() 92 } 93 onCreatenull94 override fun onCreate(savedInstanceState: Bundle?) { 95 super.onCreate(savedInstanceState) 96 97 intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)?.let { 98 component = it 99 } ?: run(this::finish) 100 isFromFavoriting = intent.getBooleanExtra(EXTRA_FROM_FAVORITING, false) 101 intent.getCharSequenceExtra(EXTRA_STRUCTURE)?.let { 102 structure = it 103 } ?: run(this::finish) 104 105 bindViews() 106 107 bindButtons() 108 } 109 onStartnull110 override fun onStart() { 111 super.onStart() 112 setUpList() 113 114 userTracker.addCallback(userTrackerCallback, mainExecutor) 115 116 if (DEBUG) { 117 Log.d(TAG, "Registered onBackInvokedCallback") 118 } 119 onBackInvokedDispatcher.registerOnBackInvokedCallback( 120 OnBackInvokedDispatcher.PRIORITY_DEFAULT, mOnBackInvokedCallback) 121 } 122 onStopnull123 override fun onStop() { 124 super.onStop() 125 userTracker.removeCallback(userTrackerCallback) 126 127 if (DEBUG) { 128 Log.d(TAG, "Unregistered onBackInvokedCallback") 129 } 130 onBackInvokedDispatcher.unregisterOnBackInvokedCallback(mOnBackInvokedCallback) 131 } 132 onBackPressednull133 override fun onBackPressed() { 134 animateExitAndFinish() 135 } 136 animateExitAndFinishnull137 private fun animateExitAndFinish() { 138 val rootView = requireViewById<ViewGroup>(R.id.controls_management_root) 139 ControlsAnimations.exitAnimation( 140 rootView, 141 object : Runnable { 142 override fun run() { 143 finish() 144 } 145 } 146 ).start() 147 } 148 bindViewsnull149 private fun bindViews() { 150 setContentView(R.layout.controls_management) 151 152 lifecycle.addObserver( 153 ControlsAnimations.observerForAnimations( 154 requireViewById<ViewGroup>(R.id.controls_management_root), 155 window, 156 intent 157 ) 158 ) 159 160 requireViewById<ViewStub>(R.id.stub).apply { 161 layoutResource = R.layout.controls_management_editing 162 inflate() 163 } 164 requireViewById<TextView>(R.id.title).text = structure 165 setTitle(structure) 166 subtitle = requireViewById<TextView>(R.id.subtitle).apply { 167 setText(SUBTITLE_ID) 168 } 169 } 170 bindButtonsnull171 private fun bindButtons() { 172 addControls = requireViewById<Button>(R.id.addControls).apply { 173 isEnabled = true 174 visibility = View.VISIBLE 175 setOnClickListener { 176 if (saveButton.isEnabled) { 177 // The user has made changes 178 Toast.makeText( 179 applicationContext, 180 R.string.controls_favorite_toast_no_changes, 181 Toast.LENGTH_SHORT 182 ).show() 183 } 184 if (isFromFavoriting) { 185 animateExitAndFinish() 186 } else { 187 startActivity(Intent(context, ControlsFavoritingActivity::class.java).also { 188 it.putExtra(ControlsFavoritingActivity.EXTRA_STRUCTURE, structure) 189 it.putExtra(Intent.EXTRA_COMPONENT_NAME, component) 190 it.putExtra( 191 ControlsFavoritingActivity.EXTRA_APP, 192 intent.getCharSequenceExtra(EXTRA_APP), 193 ) 194 it.putExtra( 195 ControlsFavoritingActivity.EXTRA_SOURCE, 196 ControlsFavoritingActivity.EXTRA_SOURCE_VALUE_FROM_EDITING, 197 ) 198 }, 199 ActivityOptions.makeSceneTransitionAnimation( 200 this@ControlsEditingActivity 201 ).toBundle(), 202 ) 203 } 204 } 205 } 206 saveButton = requireViewById<Button>(R.id.done).apply { 207 isEnabled = isFromFavoriting 208 setText(R.string.save) 209 setOnClickListener { 210 saveFavorites() 211 startActivity( 212 Intent(applicationContext, ControlsActivity::class.java), 213 ActivityOptions 214 .makeSceneTransitionAnimation(this@ControlsEditingActivity).toBundle() 215 ) 216 animateExitAndFinish() 217 } 218 } 219 } 220 saveFavoritesnull221 private fun saveFavorites() { 222 controller.replaceFavoritesForStructure( 223 StructureInfo(component, structure, model.favorites)) 224 } 225 226 private val favoritesModelCallback = object : FavoritesModel.FavoritesModelCallback { onNoneChangednull227 override fun onNoneChanged(showNoFavorites: Boolean) { 228 if (showNoFavorites) { 229 subtitle.setText(EMPTY_TEXT_ID) 230 } else { 231 subtitle.setText(SUBTITLE_ID) 232 } 233 } 234 onChangenull235 override fun onChange() = Unit 236 237 override fun onFirstChange() { 238 saveButton.isEnabled = true 239 } 240 } 241 setUpListnull242 private fun setUpList() { 243 val controls = controller.getFavoritesForStructure(component, structure) 244 model = FavoritesModel(customIconCache, component, controls, favoritesModelCallback) 245 val elevation = resources.getFloat(R.dimen.control_card_elevation) 246 val recyclerView = requireViewById<RecyclerView>(R.id.list) 247 recyclerView.alpha = 0.0f 248 val adapter = ControlAdapter(elevation, userTracker.userId).apply { 249 registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { 250 var hasAnimated = false 251 override fun onChanged() { 252 if (!hasAnimated) { 253 hasAnimated = true 254 ControlsAnimations.enterAnimation(recyclerView).start() 255 } 256 } 257 }) 258 } 259 260 val margin = resources 261 .getDimensionPixelSize(R.dimen.controls_card_margin) 262 val itemDecorator = MarginItemDecorator(margin, margin) 263 val spanCount = ControlAdapter.findMaxColumns(resources) 264 265 recyclerView.apply { 266 this.adapter = adapter 267 layoutManager = object : GridLayoutManager(recyclerView.context, spanCount) { 268 269 // This will remove from the announcement the row corresponding to the divider, 270 // as it's not something that should be announced. 271 override fun getRowCountForAccessibility( 272 recycler: RecyclerView.Recycler, 273 state: RecyclerView.State 274 ): Int { 275 val initial = super.getRowCountForAccessibility(recycler, state) 276 return if (initial > 0) initial - 1 else initial 277 } 278 }.apply { 279 spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { 280 override fun getSpanSize(position: Int): Int { 281 return if (adapter?.getItemViewType(position) 282 != ControlAdapter.TYPE_CONTROL) spanCount else 1 283 } 284 } 285 } 286 addItemDecoration(itemDecorator) 287 } 288 adapter.changeModel(model) 289 model.attachAdapter(adapter) 290 ItemTouchHelper(model.itemTouchHelperCallback).attachToRecyclerView(recyclerView) 291 } 292 onDestroynull293 override fun onDestroy() { 294 userTracker.removeCallback(userTrackerCallback) 295 super.onDestroy() 296 } 297 } 298