Create AppDataUsageAppSettingsController

For better organization and testings.

Bug: 240931350
Test: manual - on AppDataUsage
Test: unit test
Change-Id: Ie3d35f5d112cf06cca585c9859624d705fbe2655
diff --git a/res/xml/app_data_usage.xml b/res/xml/app_data_usage.xml
index d5d646c..7d4eaab 100644
--- a/res/xml/app_data_usage.xml
+++ b/res/xml/app_data_usage.xml
@@ -56,7 +56,8 @@
 
         <Preference
             android:key="app_settings"
-            android:title="@string/data_usage_app_settings" />
+            android:title="@string/data_usage_app_settings"
+            settings:controller="com.android.settings.datausage.AppDataUsageAppSettingsController" />
 
         <com.android.settingslib.RestrictedSwitchPreference
             android:key="restrict_background"
diff --git a/src/com/android/settings/datausage/AppDataUsage.java b/src/com/android/settings/datausage/AppDataUsage.java
index e0dbcbf..67d33a7 100644
--- a/src/com/android/settings/datausage/AppDataUsage.java
+++ b/src/com/android/settings/datausage/AppDataUsage.java
@@ -21,7 +21,6 @@
 import android.app.Activity;
 import android.app.settings.SettingsEnums;
 import android.content.Context;
-import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
@@ -34,7 +33,6 @@
 import android.util.IconDrawableFactory;
 import android.util.Log;
 import android.util.Range;
-import android.util.SparseBooleanArray;
 import android.view.View;
 import android.widget.AdapterView;
 
@@ -78,7 +76,6 @@
     private static final String KEY_TOTAL_USAGE = "total_usage";
     private static final String KEY_FOREGROUND_USAGE = "foreground_usage";
     private static final String KEY_BACKGROUND_USAGE = "background_usage";
-    private static final String KEY_APP_SETTINGS = "app_settings";
     private static final String KEY_RESTRICT_BACKGROUND = "restrict_background";
     private static final String KEY_CYCLE = "cycle";
     private static final String KEY_UNRESTRICTED_DATA = "unrestricted_data_saver";
@@ -90,7 +87,6 @@
     private Preference mTotalUsage;
     private Preference mForegroundUsage;
     private Preference mBackgroundUsage;
-    private Preference mAppSettings;
     private RestrictedSwitchPreference mRestrictBackground;
 
     private Drawable mIcon;
@@ -105,7 +101,6 @@
     @VisibleForTesting
     NetworkTemplate mTemplate;
     private AppItem mAppItem;
-    private Intent mAppSettingsIntent;
     private SpinnerPreference mCycle;
     private RestrictedSwitchPreference mUnrestrictedData;
     private DataSaverBackend mDataSaverBackend;
@@ -169,7 +164,6 @@
 
         final UidDetailProvider uidDetailProvider = getUidDetailProvider();
 
-        final var appDataUsageListController = use(AppDataUsageListController.class);
         if (mAppItem.key > 0) {
             if ((!isSimHardwareVisible(mContext)) || !UserHandle.isApp(mAppItem.key)) {
                 final UidDetail uidDetail = uidDetailProvider.getUidDetail(mAppItem.key, true);
@@ -179,14 +173,16 @@
                 removePreference(KEY_RESTRICT_BACKGROUND);
             } else {
                 if (mPackages.size() != 0) {
+                    int userId = UserHandle.getUserId(mAppItem.key);
                     try {
                         final ApplicationInfo info = mPackageManager.getApplicationInfoAsUser(
-                            mPackages.valueAt(0), 0, UserHandle.getUserId(mAppItem.key));
+                                mPackages.valueAt(0), 0, userId);
                         mIcon = IconDrawableFactory.newInstance(getActivity()).getBadgedIcon(info);
                         mLabel = info.loadLabel(mPackageManager);
                         mPackageName = info.packageName;
                     } catch (PackageManager.NameNotFoundException e) {
                     }
+                    use(AppDataUsageAppSettingsController.class).init(mPackages, userId);
                 }
                 mRestrictBackground = findPreference(KEY_RESTRICT_BACKGROUND);
                 mRestrictBackground.setOnPreferenceChangeListener(this);
@@ -194,26 +190,8 @@
                 mUnrestrictedData.setOnPreferenceChangeListener(this);
             }
             mDataSaverBackend = new DataSaverBackend(mContext);
-            mAppSettings = findPreference(KEY_APP_SETTINGS);
 
-            mAppSettingsIntent = new Intent(Intent.ACTION_MANAGE_NETWORK_USAGE);
-            mAppSettingsIntent.addCategory(Intent.CATEGORY_DEFAULT);
-
-            final PackageManager pm = getPackageManager();
-            boolean matchFound = false;
-            for (String packageName : mPackages) {
-                mAppSettingsIntent.setPackage(packageName);
-                if (pm.resolveActivity(mAppSettingsIntent, 0) != null) {
-                    matchFound = true;
-                    break;
-                }
-            }
-            if (!matchFound) {
-                removePreference(KEY_APP_SETTINGS);
-                mAppSettings = null;
-            }
-            appDataUsageListController.init(mAppItem.uids);
-
+            use(AppDataUsageListController.class).init(mAppItem.uids);
         } else {
             final Context context = getActivity();
             final UidDetail uidDetail = uidDetailProvider.getUidDetail(mAppItem.key, true);
@@ -222,9 +200,7 @@
             mPackageName = context.getPackageName();
 
             removePreference(KEY_UNRESTRICTED_DATA);
-            removePreference(KEY_APP_SETTINGS);
             removePreference(KEY_RESTRICT_BACKGROUND);
-            appDataUsageListController.init(new SparseBooleanArray());
         }
 
         addEntityHeader();
@@ -273,17 +249,6 @@
     }
 
     @Override
-    public boolean onPreferenceTreeClick(Preference preference) {
-        if (preference == mAppSettings) {
-            // TODO: target towards entire UID instead of just first package
-            getActivity().startActivityAsUser(mAppSettingsIntent, new UserHandle(
-                    UserHandle.getUserId(mAppItem.key)));
-            return true;
-        }
-        return super.onPreferenceTreeClick(preference);
-    }
-
-    @Override
     protected int getPreferenceScreenResId() {
         return R.xml.app_data_usage;
     }
diff --git a/src/com/android/settings/datausage/AppDataUsageAppSettingsController.kt b/src/com/android/settings/datausage/AppDataUsageAppSettingsController.kt
new file mode 100644
index 0000000..53a18c6
--- /dev/null
+++ b/src/com/android/settings/datausage/AppDataUsageAppSettingsController.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.content.Context
+import android.content.Intent
+import android.os.UserHandle
+import androidx.annotation.OpenForTesting
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import com.android.settings.core.BasePreferenceController
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@OpenForTesting
+open class AppDataUsageAppSettingsController(context: Context, preferenceKey: String) :
+    BasePreferenceController(context, preferenceKey) {
+
+    private var packageNames: Iterable<String> = emptyList()
+    private var userId: Int = -1
+    private lateinit var preference: Preference
+    private var resolvedIntent: Intent? = null
+
+    private val packageManager = mContext.packageManager
+
+    override fun getAvailabilityStatus() = AVAILABLE
+
+    fun init(packageNames: Iterable<String>, userId: Int) {
+        this.packageNames = packageNames
+        this.userId = userId
+    }
+
+    override fun displayPreference(screen: PreferenceScreen) {
+        super.displayPreference(screen)
+        preference = screen.findPreference(preferenceKey)!!
+    }
+
+    override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
+        viewLifecycleOwner.lifecycleScope.launch {
+            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                update()
+            }
+        }
+    }
+
+    private suspend fun update() {
+        resolvedIntent = withContext(Dispatchers.Default) {
+            packageNames.map { packageName ->
+                Intent(SettingsIntent).setPackage(packageName)
+            }.firstOrNull { intent ->
+                packageManager.resolveActivityAsUser(intent, 0, userId) != null
+            }
+        }
+        preference.isVisible = resolvedIntent != null
+    }
+
+    override fun handlePreferenceTreeClick(preference: Preference): Boolean {
+        if (preference.key == mPreferenceKey) {
+            resolvedIntent?.let { mContext.startActivityAsUser(it, UserHandle.of(userId)) }
+            return true
+        }
+        return false
+    }
+
+    private companion object {
+        val SettingsIntent = Intent(Intent.ACTION_MANAGE_NETWORK_USAGE).apply {
+            addCategory(Intent.CATEGORY_DEFAULT)
+        }
+    }
+}
diff --git a/src/com/android/settings/datausage/AppDataUsageListController.kt b/src/com/android/settings/datausage/AppDataUsageListController.kt
index ec944f4..e39ed7e 100644
--- a/src/com/android/settings/datausage/AppDataUsageListController.kt
+++ b/src/com/android/settings/datausage/AppDataUsageListController.kt
@@ -40,7 +40,7 @@
     private val repository: AppPreferenceRepository = AppPreferenceRepository(context),
 ) : BasePreferenceController(context, preferenceKey) {
 
-    private lateinit var uids: List<Int>
+    private var uids: List<Int> = emptyList()
     private lateinit var preference: PreferenceGroup
 
     fun init(uids: SparseBooleanArray) {
diff --git a/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageAppSettingsControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageAppSettingsControllerTest.kt
new file mode 100644
index 0000000..220c970
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/datausage/AppDataUsageAppSettingsControllerTest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argThat
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+class AppDataUsageAppSettingsControllerTest {
+    private val packageManager = mock<PackageManager>()
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+        on { packageManager } doReturn packageManager
+    }
+
+    private val controller = AppDataUsageAppSettingsController(context, KEY)
+
+    private val preference = PreferenceCategory(context).apply { key = KEY }
+
+    private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context)
+
+    @Before
+    fun setUp() {
+        preferenceScreen.addPreference(preference)
+    }
+
+    @Test
+    fun onViewCreated_noSettingsActivity_hidePreference(): Unit = runBlocking {
+        controller.init(listOf(PACKAGE_NAME), USER_ID)
+        controller.displayPreference(preferenceScreen)
+
+        controller.onViewCreated(TestLifecycleOwner())
+        delay(100)
+
+        assertThat(preference.isVisible).isFalse()
+    }
+
+    @Test
+    fun onViewCreated_hasSettingsActivity_showPreference(): Unit = runBlocking {
+        packageManager.stub {
+            on {
+                resolveActivityAsUser(
+                    argThat {
+                        action == Intent.ACTION_MANAGE_NETWORK_USAGE && getPackage() == PACKAGE_NAME
+                    },
+                    eq(0),
+                    eq(USER_ID),
+                )
+            } doReturn ResolveInfo()
+        }
+        controller.init(listOf(PACKAGE_NAME), USER_ID)
+        controller.displayPreference(preferenceScreen)
+
+        controller.onViewCreated(TestLifecycleOwner())
+        delay(100)
+
+        assertThat(preference.isVisible).isTrue()
+    }
+
+    private companion object {
+        const val KEY = "test_key"
+        const val PACKAGE_NAME = "package.name"
+        const val USER_ID = 0
+    }
+}