From 68d638707bd01a98b7bf7f3aabded107ca43e955 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 27 Apr 2023 19:47:12 +0800 Subject: [PATCH] Fix Data Saver page crashed when rotate This is because before fix mLoadAppRunnable is async run. And the getContext() within it will return null when the Fragment in some not ready lifecycle. Use viewLifecycleOwner.lifecycleScope.launch to ensure the async function will only be run when the view is ready, and automatically canceled when out of scope. Since this requires Kotlin Coroutine so migrate DataSaverSummary to Kotlin, other functionality are keep no change. Fix: 279863347 Test: Manual Change-Id: I2e97a071c103e63b3306b801fc38f4704e3be0d2 --- .../settings/datausage/DataSaverSummary.java | 234 ------------------ .../settings/datausage/DataSaverSummary.kt | 176 +++++++++++++ 2 files changed, 176 insertions(+), 234 deletions(-) delete mode 100644 src/com/android/settings/datausage/DataSaverSummary.java create mode 100644 src/com/android/settings/datausage/DataSaverSummary.kt diff --git a/src/com/android/settings/datausage/DataSaverSummary.java b/src/com/android/settings/datausage/DataSaverSummary.java deleted file mode 100644 index 67644a6c992..00000000000 --- a/src/com/android/settings/datausage/DataSaverSummary.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright (C) 2016 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.datausage; - -import android.app.Application; -import android.app.settings.SettingsEnums; -import android.content.Context; -import android.icu.text.MessageFormat; -import android.os.Bundle; -import android.telephony.SubscriptionManager; -import android.widget.Switch; - -import androidx.preference.Preference; - -import com.android.settings.R; -import com.android.settings.SettingsActivity; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.applications.AppStateBaseBridge.Callback; -import com.android.settings.datausage.DataSaverBackend.Listener; -import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.widget.SettingsMainSwitchBar; -import com.android.settingslib.applications.ApplicationsState; -import com.android.settingslib.applications.ApplicationsState.AppEntry; -import com.android.settingslib.applications.ApplicationsState.Callbacks; -import com.android.settingslib.applications.ApplicationsState.Session; -import com.android.settingslib.search.SearchIndexable; -import com.android.settingslib.widget.OnMainSwitchChangeListener; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -@SearchIndexable -public class DataSaverSummary extends SettingsPreferenceFragment - implements OnMainSwitchChangeListener, Listener, Callback, Callbacks { - - private static final String KEY_UNRESTRICTED_ACCESS = "unrestricted_access"; - - private SettingsMainSwitchBar mSwitchBar; - private DataSaverBackend mDataSaverBackend; - private Preference mUnrestrictedAccess; - private ApplicationsState mApplicationsState; - private AppStateDataUsageBridge mDataUsageBridge; - private Session mSession; - - // Flag used to avoid infinite loop due if user switch it on/off too quicky. - private boolean mSwitching; - - private Runnable mLoadAppRunnable = () -> { - mApplicationsState = ApplicationsState.getInstance( - (Application) getContext().getApplicationContext()); - mDataUsageBridge = new AppStateDataUsageBridge(mApplicationsState, this, mDataSaverBackend); - mSession = mApplicationsState.newSession(this, getSettingsLifecycle()); - mDataUsageBridge.resume(true /* forceLoadAllApps */); - }; - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - if (!isDataSaverVisible(getContext())) { - finishFragment(); - return; - } - - addPreferencesFromResource(R.xml.data_saver); - mUnrestrictedAccess = findPreference(KEY_UNRESTRICTED_ACCESS); - mDataSaverBackend = new DataSaverBackend(getContext()); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mSwitchBar = ((SettingsActivity) getActivity()).getSwitchBar(); - mSwitchBar.setTitle(getContext().getString(R.string.data_saver_switch_title)); - mSwitchBar.show(); - mSwitchBar.addOnSwitchChangeListener(this); - } - - @Override - public void onResume() { - super.onResume(); - mDataSaverBackend.refreshAllowlist(); - mDataSaverBackend.refreshDenylist(); - mDataSaverBackend.addListener(this); - if (mDataUsageBridge != null) { - mDataUsageBridge.resume(true /* forceLoadAllApps */); - } else { - getView().post(mLoadAppRunnable); - } - } - - @Override - public void onPause() { - super.onPause(); - mDataSaverBackend.remListener(this); - if (mDataUsageBridge != null) { - mDataUsageBridge.pause(); - } - } - - @Override - public void onSwitchChanged(Switch switchView, boolean isChecked) { - synchronized (this) { - if (mSwitching) { - return; - } - mSwitching = true; - mDataSaverBackend.setDataSaverEnabled(isChecked); - } - } - - @Override - public int getMetricsCategory() { - return SettingsEnums.DATA_SAVER_SUMMARY; - } - - @Override - public int getHelpResource() { - return R.string.help_url_data_saver; - } - - @Override - public void onDataSaverChanged(boolean isDataSaving) { - synchronized (this) { - mSwitchBar.setChecked(isDataSaving); - mSwitching = false; - } - } - - @Override - public void onAllowlistStatusChanged(int uid, boolean isAllowlisted) { - } - - @Override - public void onDenylistStatusChanged(int uid, boolean isDenylisted) { - } - - @Override - public void onExtraInfoUpdated() { - updateUnrestrictedAccessSummary(); - } - - @Override - public void onRunningStateChanged(boolean running) { - - } - - @Override - public void onPackageListChanged() { - - } - - @Override - public void onRebuildComplete(ArrayList apps) { - - } - - @Override - public void onPackageIconChanged() { - - } - - @Override - public void onPackageSizeChanged(String packageName) { - - } - - @Override - public void onAllSizesComputed() { - updateUnrestrictedAccessSummary(); - } - - @Override - public void onLauncherInfoChanged() { - updateUnrestrictedAccessSummary(); - } - - @Override - public void onLoadEntriesCompleted() { - - } - - private void updateUnrestrictedAccessSummary() { - if (!isAdded() || isFinishingOrDestroyed() || mSession == null) return; - - int count = 0; - for (AppEntry entry : mSession.getAllApps()) { - if (!ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER.filterApp(entry)) { - continue; - } - if (entry.extraInfo != null && ((AppStateDataUsageBridge.DataUsageState) - entry.extraInfo).isDataSaverAllowlisted) { - count++; - } - } - MessageFormat msgFormat = new MessageFormat( - getResources().getString(R.string.data_saver_unrestricted_summary), - Locale.getDefault()); - Map arguments = new HashMap<>(); - arguments.put("count", count); - mUnrestrictedAccess.setSummary(msgFormat.format(arguments)); - } - - public static boolean isDataSaverVisible(Context context) { - return context.getResources() - .getBoolean(R.bool.config_show_data_saver); - } - - public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider(R.xml.data_saver) { - - @Override - protected boolean isPageSearchEnabled(Context context) { - return isDataSaverVisible(context) - && DataUsageUtils.hasMobileData(context) - && DataUsageUtils.getDefaultSubscriptionId(context) - != SubscriptionManager.INVALID_SUBSCRIPTION_ID; - } - }; -} diff --git a/src/com/android/settings/datausage/DataSaverSummary.kt b/src/com/android/settings/datausage/DataSaverSummary.kt new file mode 100644 index 00000000000..1d9cbb73a66 --- /dev/null +++ b/src/com/android/settings/datausage/DataSaverSummary.kt @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2023 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.datausage + +import android.app.Application +import android.app.settings.SettingsEnums +import android.content.Context +import android.os.Bundle +import android.telephony.SubscriptionManager +import android.widget.Switch +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import com.android.settings.R +import com.android.settings.SettingsActivity +import com.android.settings.SettingsPreferenceFragment +import com.android.settings.applications.AppStateBaseBridge +import com.android.settings.datausage.AppStateDataUsageBridge.DataUsageState +import com.android.settings.search.BaseSearchIndexProvider +import com.android.settings.widget.SettingsMainSwitchBar +import com.android.settingslib.applications.ApplicationsState +import com.android.settingslib.search.SearchIndexable +import com.android.settingslib.spa.framework.util.formatString +import kotlinx.coroutines.launch + +@SearchIndexable +class DataSaverSummary : SettingsPreferenceFragment() { + private lateinit var switchBar: SettingsMainSwitchBar + private lateinit var dataSaverBackend: DataSaverBackend + private lateinit var unrestrictedAccess: Preference + private var dataUsageBridge: AppStateDataUsageBridge? = null + private var session: ApplicationsState.Session? = null + + // Flag used to avoid infinite loop due if user switch it on/off too quick. + private var switching = false + + override fun onCreate(bundle: Bundle?) { + super.onCreate(bundle) + + if (!requireContext().isDataSaverVisible()) { + finishFragment() + return + } + + addPreferencesFromResource(R.xml.data_saver) + unrestrictedAccess = findPreference(KEY_UNRESTRICTED_ACCESS)!! + dataSaverBackend = DataSaverBackend(requireContext()) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + switchBar = (activity as SettingsActivity).switchBar.apply { + setTitle(getString(R.string.data_saver_switch_title)) + show() + addOnSwitchChangeListener { _: Switch, isChecked: Boolean -> + onSwitchChanged(isChecked) + } + } + } + + override fun onResume() { + super.onResume() + dataSaverBackend.refreshAllowlist() + dataSaverBackend.refreshDenylist() + dataSaverBackend.addListener(dataSaverBackendListener) + dataUsageBridge?.resume(/* forceLoadAllApps= */ true) + ?: viewLifecycleOwner.lifecycleScope.launch { + val applicationsState = ApplicationsState.getInstance( + requireContext().applicationContext as Application + ) + dataUsageBridge = AppStateDataUsageBridge( + applicationsState, dataUsageBridgeCallbacks, dataSaverBackend + ) + session = + applicationsState.newSession(applicationsStateCallbacks, settingsLifecycle) + dataUsageBridge?.resume(/* forceLoadAllApps= */ true) + } + } + + override fun onPause() { + super.onPause() + dataSaverBackend.remListener(dataSaverBackendListener) + dataUsageBridge?.pause() + } + + private fun onSwitchChanged(isChecked: Boolean) { + synchronized(this) { + if (!switching) { + switching = true + dataSaverBackend.isDataSaverEnabled = isChecked + } + } + } + + override fun getMetricsCategory() = SettingsEnums.DATA_SAVER_SUMMARY + + override fun getHelpResource() = R.string.help_url_data_saver + + private val dataSaverBackendListener = object : DataSaverBackend.Listener { + override fun onDataSaverChanged(isDataSaving: Boolean) { + synchronized(this) { + switchBar.isChecked = isDataSaving + switching = false + } + } + + override fun onAllowlistStatusChanged(uid: Int, isAllowlisted: Boolean) {} + + override fun onDenylistStatusChanged(uid: Int, isDenylisted: Boolean) {} + } + + private val dataUsageBridgeCallbacks = AppStateBaseBridge.Callback { + updateUnrestrictedAccessSummary() + } + + private val applicationsStateCallbacks = object : ApplicationsState.Callbacks { + override fun onRunningStateChanged(running: Boolean) {} + + override fun onPackageListChanged() {} + + override fun onRebuildComplete(apps: ArrayList?) {} + + override fun onPackageIconChanged() {} + + override fun onPackageSizeChanged(packageName: String?) {} + + override fun onAllSizesComputed() { + updateUnrestrictedAccessSummary() + } + + override fun onLauncherInfoChanged() { + updateUnrestrictedAccessSummary() + } + + override fun onLoadEntriesCompleted() {} + } + + private fun updateUnrestrictedAccessSummary() { + if (!isAdded || isFinishingOrDestroyed) return + val allApps = session?.allApps ?: return + val count = allApps.count { + ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER.filterApp(it) && + (it.extraInfo as? DataUsageState)?.isDataSaverAllowlisted == true + } + unrestrictedAccess.summary = + resources.formatString(R.string.data_saver_unrestricted_summary, "count" to count) + } + + companion object { + private const val KEY_UNRESTRICTED_ACCESS = "unrestricted_access" + + private fun Context.isDataSaverVisible(): Boolean = + resources.getBoolean(R.bool.config_show_data_saver) + + @JvmField + val SEARCH_INDEX_DATA_PROVIDER = object : BaseSearchIndexProvider(R.xml.data_saver) { + override fun isPageSearchEnabled(context: Context): Boolean = + context.isDataSaverVisible() && + DataUsageUtils.hasMobileData(context) && + (DataUsageUtils.getDefaultSubscriptionId(context) != + SubscriptionManager.INVALID_SUBSCRIPTION_ID) + } + } +} \ No newline at end of file