From bb9cc0817274c5fbdfcc87916318315ad9324ff2 Mon Sep 17 00:00:00 2001 From: Jacky Wang Date: Fri, 8 Nov 2024 14:55:11 +0800 Subject: [PATCH] [Catalyst] Rebind preference immediately when restriction is changed Bug: 377600992 Flag: com.android.settings.flags.catalyst Test: testdpc Change-Id: Iee382afb8395355ee77d604bd399972326557cc0 --- .../settings/dashboard/DashboardFragment.java | 20 +++++ .../UserRestrictionBindingHelper.kt | 66 +++++++++++++++ .../settings/restriction/UserRestrictions.kt | 83 +++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 src/com/android/settings/restriction/UserRestrictionBindingHelper.kt create mode 100644 src/com/android/settings/restriction/UserRestrictions.kt diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java index 92e99cf12f9..c6d1222adca 100644 --- a/src/com/android/settings/dashboard/DashboardFragment.java +++ b/src/com/android/settings/dashboard/DashboardFragment.java @@ -48,11 +48,13 @@ import com.android.settings.core.CategoryMixin.CategoryListener; import com.android.settings.core.PreferenceControllerListHelper; import com.android.settings.flags.Flags; import com.android.settings.overlay.FeatureFactory; +import com.android.settings.restriction.UserRestrictionBindingHelper; import com.android.settingslib.PrimarySwitchPreference; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.drawer.DashboardCategory; import com.android.settingslib.drawer.Tile; +import com.android.settingslib.preference.PreferenceScreenBindingHelper; import com.android.settingslib.preference.PreferenceScreenCreator; import com.android.settingslib.search.Indexable; @@ -92,6 +94,8 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment private boolean mListeningToCategoryChange; private List mSuppressInjectedTileKeys; + private @Nullable UserRestrictionBindingHelper mUserRestrictionBindingHelper; + @Override public void onAttach(Context context) { super.onAttach(context); @@ -178,6 +182,13 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment // editing dialog is recreated (that would happen before onResume is called). updatePreferenceStates(); } + if (isCatalystEnabled()) { + PreferenceScreenBindingHelper helper = getPreferenceScreenBindingHelper(); + if (helper != null) { + mUserRestrictionBindingHelper = new UserRestrictionBindingHelper(requireContext(), + helper); + } + } } @Override @@ -288,6 +299,15 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment } } + @Override + public void onDestroy() { + if (mUserRestrictionBindingHelper != null) { + mUserRestrictionBindingHelper.close(); + mUserRestrictionBindingHelper = null; + } + super.onDestroy(); + } + @Override protected final int getPreferenceScreenResId(@NonNull Context context) { return getPreferenceScreenResId(); diff --git a/src/com/android/settings/restriction/UserRestrictionBindingHelper.kt b/src/com/android/settings/restriction/UserRestrictionBindingHelper.kt new file mode 100644 index 00000000000..fb7466aabd0 --- /dev/null +++ b/src/com/android/settings/restriction/UserRestrictionBindingHelper.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 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.settings.restriction + +import android.content.Context +import com.android.settings.PreferenceRestrictionMixin +import com.android.settingslib.datastore.HandlerExecutor +import com.android.settingslib.datastore.KeyedObserver +import com.android.settingslib.preference.PreferenceScreenBindingHelper +import com.android.settingslib.preference.PreferenceScreenBindingHelper.Companion.CHANGE_REASON_STATE + +/** Helper to rebind preference immediately when user restriction is changed. */ +class UserRestrictionBindingHelper( + context: Context, + private val screenBindingHelper: PreferenceScreenBindingHelper, +) : AutoCloseable { + private val restrictionKeysToPreferenceKeys: Map> = + mutableMapOf>() + .apply { + screenBindingHelper.forEachRecursively { + val metadata = it.metadata + if (metadata is PreferenceRestrictionMixin) { + getOrPut(metadata.restrictionKey) { mutableSetOf() }.add(metadata.key) + } + } + } + .toMap() + + private val userRestrictionObserver: KeyedObserver? + + init { + if (restrictionKeysToPreferenceKeys.isEmpty()) { + userRestrictionObserver = null + } else { + val observer = + KeyedObserver { restrictionKey, _ -> + restrictionKey?.let { notifyRestrictionChanged(it) } + } + UserRestrictions.addObserver(context, observer, HandlerExecutor.main) + userRestrictionObserver = observer + } + } + + private fun notifyRestrictionChanged(restrictionKey: String) { + val keys = restrictionKeysToPreferenceKeys[restrictionKey] ?: return + for (key in keys) screenBindingHelper.notifyChange(key, CHANGE_REASON_STATE) + } + + override fun close() { + userRestrictionObserver?.let { UserRestrictions.removeObserver(it) } + } +} diff --git a/src/com/android/settings/restriction/UserRestrictions.kt b/src/com/android/settings/restriction/UserRestrictions.kt new file mode 100644 index 00000000000..1fa68307a4a --- /dev/null +++ b/src/com/android/settings/restriction/UserRestrictions.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 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.settings.restriction + +import android.content.Context +import android.os.Bundle +import android.os.IUserRestrictionsListener +import android.os.UserManager +import com.android.settingslib.datastore.KeyedDataObservable +import com.android.settingslib.datastore.KeyedObserver +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicBoolean + +/** Helper class to monitor user restriction changes. */ +object UserRestrictions { + private val observable = KeyedDataObservable() + + private val userRestrictionsListener = + object : IUserRestrictionsListener.Stub() { + override fun onUserRestrictionsChanged( + userId: Int, + newRestrictions: Bundle, + prevRestrictions: Bundle, + ) { + // there is no API to remove listener, do a quick check to avoid unnecessary work + if (!observable.hasAnyObserver()) return + + val changedKeys = mutableSetOf() + val keys = newRestrictions.keySet() + prevRestrictions.keySet() + for (key in keys) { + if (newRestrictions.getBoolean(key) != prevRestrictions.getBoolean(key)) { + changedKeys.add(key) + } + } + + for (key in changedKeys) observable.notifyChange(key, 0) + } + } + + private val listenerAdded = AtomicBoolean() + + fun addObserver(context: Context, observer: KeyedObserver, executor: Executor) { + context.addUserRestrictionsListener() + observable.addObserver(observer, executor) + } + + fun addObserver( + context: Context, + key: String, + observer: KeyedObserver, + executor: Executor, + ) { + context.addUserRestrictionsListener() + observable.addObserver(key, observer, executor) + } + + private fun Context.addUserRestrictionsListener() { + if (listenerAdded.getAndSet(true)) return + // surprisingly, there is no way to remove the listener + applicationContext + .getSystemService(UserManager::class.java) + .addUserRestrictionsListener(userRestrictionsListener) + } + + fun removeObserver(observer: KeyedObserver) = observable.removeObserver(observer) + + fun removeObserver(key: String, observer: KeyedObserver) = + observable.removeObserver(key, observer) +}