| /* |
| * Copyright (C) 2020 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.systemui.controls.management |
| |
| import android.animation.Animator |
| import android.animation.AnimatorListenerAdapter |
| import android.app.ActivityOptions |
| import android.content.ComponentName |
| import android.content.Intent |
| import android.content.res.Configuration |
| import android.os.Bundle |
| import android.text.TextUtils |
| import android.view.Gravity |
| import android.view.View |
| import android.view.ViewGroup |
| import android.view.ViewStub |
| import android.widget.Button |
| import android.widget.FrameLayout |
| import android.widget.TextView |
| import android.widget.Toast |
| import androidx.viewpager2.widget.ViewPager2 |
| import com.android.systemui.Prefs |
| import com.android.systemui.R |
| import com.android.systemui.broadcast.BroadcastDispatcher |
| import com.android.systemui.controls.ControlsServiceInfo |
| import com.android.systemui.controls.TooltipManager |
| import com.android.systemui.controls.controller.ControlsControllerImpl |
| import com.android.systemui.controls.controller.StructureInfo |
| import com.android.systemui.controls.ui.ControlsActivity |
| import com.android.systemui.controls.ui.ControlsUiController |
| import com.android.systemui.dagger.qualifiers.Main |
| import com.android.systemui.settings.CurrentUserTracker |
| import com.android.systemui.util.LifecycleActivity |
| import java.text.Collator |
| import java.util.concurrent.Executor |
| import java.util.function.Consumer |
| import javax.inject.Inject |
| |
| class ControlsFavoritingActivity @Inject constructor( |
| @Main private val executor: Executor, |
| private val controller: ControlsControllerImpl, |
| private val listingController: ControlsListingController, |
| private val broadcastDispatcher: BroadcastDispatcher, |
| private val uiController: ControlsUiController |
| ) : LifecycleActivity() { |
| |
| companion object { |
| private const val TAG = "ControlsFavoritingActivity" |
| |
| // If provided and no structure is available, use as the title |
| const val EXTRA_APP = "extra_app_label" |
| |
| // If provided, show this structure page first |
| const val EXTRA_STRUCTURE = "extra_structure" |
| const val EXTRA_SINGLE_STRUCTURE = "extra_single_structure" |
| internal const val EXTRA_FROM_PROVIDER_SELECTOR = "extra_from_provider_selector" |
| private const val TOOLTIP_PREFS_KEY = Prefs.Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT |
| private const val TOOLTIP_MAX_SHOWN = 2 |
| } |
| |
| private var component: ComponentName? = null |
| private var appName: CharSequence? = null |
| private var structureExtra: CharSequence? = null |
| private var fromProviderSelector = false |
| |
| private lateinit var structurePager: ViewPager2 |
| private lateinit var statusText: TextView |
| private lateinit var titleView: TextView |
| private lateinit var subtitleView: TextView |
| private lateinit var pageIndicator: ManagementPageIndicator |
| private var mTooltipManager: TooltipManager? = null |
| private lateinit var doneButton: View |
| private lateinit var otherAppsButton: View |
| private var listOfStructures = emptyList<StructureContainer>() |
| |
| private lateinit var comparator: Comparator<StructureContainer> |
| private var cancelLoadRunnable: Runnable? = null |
| private var isPagerLoaded = false |
| |
| private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) { |
| private val startingUser = controller.currentUserId |
| |
| override fun onUserSwitched(newUserId: Int) { |
| if (newUserId != startingUser) { |
| stopTracking() |
| finish() |
| } |
| } |
| } |
| |
| private val listingCallback = object : ControlsListingController.ControlsListingCallback { |
| |
| override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) { |
| if (serviceInfos.size > 1) { |
| otherAppsButton.post { |
| otherAppsButton.visibility = View.VISIBLE |
| } |
| } |
| } |
| } |
| |
| override fun onBackPressed() { |
| if (!fromProviderSelector) { |
| openControlsOrigin() |
| } |
| animateExitAndFinish() |
| } |
| |
| override fun onCreate(savedInstanceState: Bundle?) { |
| super.onCreate(savedInstanceState) |
| |
| val collator = Collator.getInstance(resources.configuration.locales[0]) |
| comparator = compareBy(collator) { it.structureName } |
| appName = intent.getCharSequenceExtra(EXTRA_APP) |
| structureExtra = intent.getCharSequenceExtra(EXTRA_STRUCTURE) |
| component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME) |
| fromProviderSelector = intent.getBooleanExtra(EXTRA_FROM_PROVIDER_SELECTOR, false) |
| |
| bindViews() |
| } |
| |
| private val controlsModelCallback = object : ControlsModel.ControlsModelCallback { |
| override fun onFirstChange() { |
| doneButton.isEnabled = true |
| } |
| } |
| |
| private fun loadControls() { |
| component?.let { |
| statusText.text = resources.getText(com.android.internal.R.string.loading) |
| val emptyZoneString = resources.getText( |
| R.string.controls_favorite_other_zone_header) |
| controller.loadForComponent(it, Consumer { data -> |
| val allControls = data.allControls |
| val favoriteKeys = data.favoritesIds |
| val error = data.errorOnLoad |
| val controlsByStructure = allControls.groupBy { it.control.structure ?: "" } |
| listOfStructures = controlsByStructure.map { |
| StructureContainer(it.key, AllModel( |
| it.value, favoriteKeys, emptyZoneString, controlsModelCallback)) |
| }.sortedWith(comparator) |
| |
| val structureIndex = listOfStructures.indexOfFirst { |
| sc -> sc.structureName == structureExtra |
| }.let { if (it == -1) 0 else it } |
| |
| // If we were requested to show a single structure, set the list to just that one |
| if (intent.getBooleanExtra(EXTRA_SINGLE_STRUCTURE, false)) { |
| listOfStructures = listOf(listOfStructures[structureIndex]) |
| } |
| |
| executor.execute { |
| structurePager.adapter = StructureAdapter(listOfStructures) |
| structurePager.setCurrentItem(structureIndex) |
| if (error) { |
| statusText.text = resources.getString(R.string.controls_favorite_load_error, |
| appName ?: "") |
| subtitleView.visibility = View.GONE |
| } else if (listOfStructures.isEmpty()) { |
| statusText.text = resources.getString(R.string.controls_favorite_load_none) |
| subtitleView.visibility = View.GONE |
| } else { |
| statusText.visibility = View.GONE |
| |
| pageIndicator.setNumPages(listOfStructures.size) |
| pageIndicator.setLocation(0f) |
| pageIndicator.visibility = |
| if (listOfStructures.size > 1) View.VISIBLE else View.INVISIBLE |
| |
| ControlsAnimations.enterAnimation(pageIndicator).apply { |
| addListener(object : AnimatorListenerAdapter() { |
| override fun onAnimationEnd(animation: Animator?) { |
| // Position the tooltip if necessary after animations are complete |
| // so we can get the position on screen. The tooltip is not |
| // rooted in the layout root. |
| if (pageIndicator.visibility == View.VISIBLE && |
| mTooltipManager != null) { |
| val p = IntArray(2) |
| pageIndicator.getLocationOnScreen(p) |
| val x = p[0] + pageIndicator.width / 2 |
| val y = p[1] + pageIndicator.height |
| mTooltipManager?.show( |
| R.string.controls_structure_tooltip, x, y) |
| } |
| } |
| }) |
| }.start() |
| ControlsAnimations.enterAnimation(structurePager).start() |
| } |
| } |
| }, Consumer { runnable -> cancelLoadRunnable = runnable }) |
| } |
| } |
| |
| private fun setUpPager() { |
| structurePager.alpha = 0.0f |
| pageIndicator.alpha = 0.0f |
| structurePager.apply { |
| adapter = StructureAdapter(emptyList()) |
| registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { |
| override fun onPageSelected(position: Int) { |
| super.onPageSelected(position) |
| val name = listOfStructures[position].structureName |
| val title = if (!TextUtils.isEmpty(name)) name else appName |
| titleView.text = title |
| titleView.requestFocus() |
| } |
| |
| override fun onPageScrolled( |
| position: Int, |
| positionOffset: Float, |
| positionOffsetPixels: Int |
| ) { |
| super.onPageScrolled(position, positionOffset, positionOffsetPixels) |
| pageIndicator.setLocation(position + positionOffset) |
| } |
| }) |
| } |
| } |
| |
| private fun bindViews() { |
| setContentView(R.layout.controls_management) |
| |
| getLifecycle().addObserver( |
| ControlsAnimations.observerForAnimations( |
| requireViewById<ViewGroup>(R.id.controls_management_root), |
| window, |
| intent |
| ) |
| ) |
| |
| requireViewById<ViewStub>(R.id.stub).apply { |
| layoutResource = R.layout.controls_management_favorites |
| inflate() |
| } |
| |
| statusText = requireViewById(R.id.status_message) |
| if (shouldShowTooltip()) { |
| mTooltipManager = TooltipManager(statusText.context, |
| TOOLTIP_PREFS_KEY, TOOLTIP_MAX_SHOWN) |
| addContentView( |
| mTooltipManager?.layout, |
| FrameLayout.LayoutParams( |
| ViewGroup.LayoutParams.WRAP_CONTENT, |
| ViewGroup.LayoutParams.WRAP_CONTENT, |
| Gravity.TOP or Gravity.LEFT |
| ) |
| ) |
| } |
| pageIndicator = requireViewById<ManagementPageIndicator>( |
| R.id.structure_page_indicator).apply { |
| visibilityListener = { |
| if (it != View.VISIBLE) { |
| mTooltipManager?.hide(true) |
| } |
| } |
| } |
| |
| val title = structureExtra |
| ?: (appName ?: resources.getText(R.string.controls_favorite_default_title)) |
| titleView = requireViewById<TextView>(R.id.title).apply { |
| text = title |
| } |
| subtitleView = requireViewById<TextView>(R.id.subtitle).apply { |
| text = resources.getText(R.string.controls_favorite_subtitle) |
| } |
| structurePager = requireViewById<ViewPager2>(R.id.structure_pager) |
| structurePager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { |
| override fun onPageSelected(position: Int) { |
| super.onPageSelected(position) |
| mTooltipManager?.hide(true) |
| } |
| }) |
| bindButtons() |
| } |
| |
| private fun animateExitAndFinish() { |
| val rootView = requireViewById<ViewGroup>(R.id.controls_management_root) |
| ControlsAnimations.exitAnimation( |
| rootView, |
| object : Runnable { |
| override fun run() { |
| finish() |
| } |
| } |
| ).start() |
| } |
| |
| private fun bindButtons() { |
| otherAppsButton = requireViewById<Button>(R.id.other_apps).apply { |
| setOnClickListener { |
| if (doneButton.isEnabled) { |
| // The user has made changes |
| Toast.makeText( |
| applicationContext, |
| R.string.controls_favorite_toast_no_changes, |
| Toast.LENGTH_SHORT |
| ).show() |
| } |
| startActivity( |
| Intent(context, ControlsProviderSelectorActivity::class.java), |
| ActivityOptions |
| .makeSceneTransitionAnimation(this@ControlsFavoritingActivity).toBundle() |
| ) |
| animateExitAndFinish() |
| } |
| } |
| |
| doneButton = requireViewById<Button>(R.id.done).apply { |
| isEnabled = false |
| setOnClickListener { |
| if (component == null) return@setOnClickListener |
| listOfStructures.forEach { |
| val favoritesForStorage = it.model.favorites |
| controller.replaceFavoritesForStructure( |
| StructureInfo(component!!, it.structureName, favoritesForStorage) |
| ) |
| } |
| animateExitAndFinish() |
| openControlsOrigin() |
| } |
| } |
| } |
| |
| private fun openControlsOrigin() { |
| startActivity( |
| Intent(applicationContext, ControlsActivity::class.java), |
| ActivityOptions.makeSceneTransitionAnimation(this).toBundle() |
| ) |
| } |
| |
| override fun onPause() { |
| super.onPause() |
| mTooltipManager?.hide(false) |
| } |
| |
| override fun onStart() { |
| super.onStart() |
| |
| listingController.addCallback(listingCallback) |
| currentUserTracker.startTracking() |
| } |
| |
| override fun onResume() { |
| super.onResume() |
| |
| // only do once, to make sure that any user changes do not get replaces if resume is called |
| // more than once |
| if (!isPagerLoaded) { |
| setUpPager() |
| loadControls() |
| isPagerLoaded = true |
| } |
| } |
| |
| override fun onStop() { |
| super.onStop() |
| |
| listingController.removeCallback(listingCallback) |
| currentUserTracker.stopTracking() |
| } |
| |
| override fun onConfigurationChanged(newConfig: Configuration) { |
| super.onConfigurationChanged(newConfig) |
| mTooltipManager?.hide(false) |
| } |
| |
| override fun onDestroy() { |
| cancelLoadRunnable?.run() |
| super.onDestroy() |
| } |
| |
| private fun shouldShowTooltip(): Boolean { |
| return Prefs.getInt(applicationContext, TOOLTIP_PREFS_KEY, 0) < TOOLTIP_MAX_SHOWN |
| } |
| } |
| |
| data class StructureContainer(val structureName: CharSequence, val model: ControlsModel) |