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