1 /*
<lambda>null2  * Copyright (C) 2023 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.settings.datausage.lib
18 
19 import android.app.usage.NetworkStats
20 import android.content.Context
21 import android.content.pm.UserProperties
22 import android.net.NetworkPolicyManager
23 import android.net.NetworkTemplate
24 import android.os.Process
25 import android.os.UserHandle
26 import android.os.UserManager
27 import android.util.SparseArray
28 import android.util.SparseBooleanArray
29 import androidx.annotation.VisibleForTesting
30 import androidx.core.util.keyIterator
31 import com.android.settings.R
32 import com.android.settings.datausage.lib.NetworkStatsRepository.Companion.Bucket
33 import com.android.settingslib.AppItem
34 import com.android.settingslib.net.UidDetailProvider
35 import com.android.settingslib.spaprivileged.framework.common.userManager
36 
37 class AppDataUsageRepository(
38     private val context: Context,
39     private val currentUserId: Int,
40     template: NetworkTemplate,
41     private val getPackageName: (AppItem) -> String?,
42 ) {
43     private val networkStatsRepository = NetworkStatsRepository(context, template)
44 
45     fun getAppPercent(carrierId: Int?, startTime: Long, endTime: Long): List<Pair<AppItem, Int>> {
46         val buckets = networkStatsRepository.queryBuckets(startTime, endTime)
47         return getAppPercent(carrierId, buckets)
48     }
49 
50     @VisibleForTesting
51     fun getAppPercent(carrierId: Int?, buckets: List<Bucket>): List<Pair<AppItem, Int>> {
52         val items = ArrayList<AppItem>()
53         val knownItems = SparseArray<AppItem>()
54         val profiles = context.userManager.userProfiles
55         val userManager : UserManager = context.getSystemService(Context.USER_SERVICE) as UserManager
56         val userIdToIsHiddenMap = profiles.associate { profile ->
57             profile.identifier to shouldSkipProfile(userManager, profile)
58         }
59         bindStats(buckets, userIdToIsHiddenMap, knownItems, items)
60         val restrictedUids = context.getSystemService(NetworkPolicyManager::class.java)!!
61             .getUidsWithPolicy(NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND)
62         for (uid in restrictedUids) {
63             // Only splice in restricted state for current user or managed users
64             if (!profiles.contains(UserHandle.getUserHandleForUid(uid))) {
65                 continue
66             }
67             var item = knownItems[uid]
68             if (item == null) {
69                 item = AppItem(uid)
70                 item.total = 0
71                 item.addUid(uid)
72                 items.add(item)
73                 knownItems.put(item.key, item)
74             }
75             item.restricted = true
76         }
77 
78         val filteredItems = filterItems(carrierId, items).sorted()
79         val largest: Long = filteredItems.maxOfOrNull { it.total } ?: 0
80         return filteredItems.map { item ->
81             val percentTotal = if (largest > 0) (item.total * 100 / largest).toInt() else 0
82             item to percentTotal
83         }
84     }
85 
86     private fun filterItems(carrierId: Int?, items: List<AppItem>): List<AppItem> {
87         // When there is no specified SubscriptionInfo, Wi-Fi data usage will be displayed.
88         // In this case, the carrier service package also needs to be hidden.
89         if (carrierId != null && carrierId !in context.resources.getIntArray(
90                 R.array.datausage_hiding_carrier_service_carrier_id
91             )
92         ) {
93             return items
94         }
95         val hiddenPackageNames = context.resources.getStringArray(
96             R.array.datausage_hiding_carrier_service_package_names
97         )
98         return items.filter { item ->
99             // Do not show carrier service package in data usage list if it should be hidden for
100             // the carrier.
101             getPackageName(item) !in hiddenPackageNames
102         }
103     }
104 
105     private fun bindStats(
106         buckets: List<Bucket>,
107         userIdToIsHiddenMap: Map<Int, Boolean>,
108         knownItems: SparseArray<AppItem>,
109         items: ArrayList<AppItem>,
110     ) {
111         for (bucket in buckets) {
112             // Decide how to collapse items together
113             val uid = bucket.uid
114             val collapseKey: Int
115             val category: Int
116             val userId = UserHandle.getUserId(uid)
117             if(userIdToIsHiddenMap[userId] == true) {
118                 continue
119             }
120             if (UserHandle.isApp(uid) || Process.isSdkSandboxUid(uid)) {
121                 if (userIdToIsHiddenMap.keys.contains(userId)) {
122                     if (userId != currentUserId) {
123                         // Add to a managed user item.
124                         accumulate(
125                             collapseKey = UidDetailProvider.buildKeyForUser(userId),
126                             knownItems = knownItems,
127                             bucket = bucket,
128                             itemCategory = AppItem.CATEGORY_USER,
129                             items = items,
130                         )
131                     }
132                     collapseKey = getAppUid(uid)
133                     category = AppItem.CATEGORY_APP
134                 } else {
135                     // If it is a removed user add it to the removed users' key
136                     if (context.userManager.getUserInfo(userId) == null) {
137                         collapseKey = NetworkStats.Bucket.UID_REMOVED
138                         category = AppItem.CATEGORY_APP
139                     } else {
140                         // Add to other user item.
141                         collapseKey = UidDetailProvider.buildKeyForUser(userId)
142                         category = AppItem.CATEGORY_USER
143                     }
144                 }
145             } else if (uid == NetworkStats.Bucket.UID_REMOVED ||
146                 uid == NetworkStats.Bucket.UID_TETHERING ||
147                 uid == Process.OTA_UPDATE_UID
148             ) {
149                 collapseKey = uid
150                 category = AppItem.CATEGORY_APP
151             } else {
152                 collapseKey = Process.SYSTEM_UID
153                 category = AppItem.CATEGORY_APP
154             }
155             accumulate(
156                 collapseKey = collapseKey,
157                 knownItems = knownItems,
158                 bucket = bucket,
159                 itemCategory = category,
160                 items = items,
161             )
162         }
163     }
164 
165     private fun shouldSkipProfile(userManager : UserManager, userHandle: UserHandle): Boolean {
166         if (android.os.Flags.allowPrivateProfile()
167                 && android.multiuser.Flags.enablePrivateSpaceFeatures()
168                 && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) {
169             return (userManager.isQuietModeEnabled(userHandle)
170                     && userManager.getUserProperties(userHandle).showInQuietMode
171                     == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN)
172         }
173         return false
174     }
175 
176     /**
177      * Accumulate data usage of a network stats entry for the item mapped by the collapse key.
178      * Creates the item if needed.
179      *
180      * @param collapseKey  the collapse key used to map the item.
181      * @param knownItems   collection of known (already existing) items.
182      * @param bucket       the network stats bucket to extract data usage from.
183      * @param itemCategory the item is categorized on the list view by this category. Must be
184      */
185     private fun accumulate(
186         collapseKey: Int,
187         knownItems: SparseArray<AppItem>,
188         bucket: Bucket,
189         itemCategory: Int,
190         items: ArrayList<AppItem>,
191     ) {
192         var item = knownItems[collapseKey]
193         if (item == null) {
194             item = AppItem(collapseKey)
195             item.category = itemCategory
196             items.add(item)
197             knownItems.put(item.key, item)
198         }
199         item.addUid(bucket.uid)
200         item.total += bucket.bytes
201     }
202 
203     companion object {
204         @JvmStatic
205         fun getAppUidList(uids: SparseBooleanArray) =
206             uids.keyIterator().asSequence().map { getAppUid(it) }.distinct().toList()
207 
208         @JvmStatic
209         fun getAppUid(uid: Int): Int {
210             if (Process.isSdkSandboxUid(uid)) {
211                 // For a sandbox process, get the associated app UID
212                 return Process.getAppUidForSdkSandboxUid(uid)
213             }
214             return uid
215         }
216 
217         /**
218          * Gets the apps' uids, also add the apps' SDK sandboxes' uids.
219          *
220          * In case we've been asked data usage for an app, include data usage of the corresponding
221          * SDK sandbox.
222          */
223         fun withSdkSandboxUids(uids: List<Int>): List<Int> {
224             val set = uids.toMutableSet()
225             for (uid in uids) {
226                 if (Process.isApplicationUid(uid)) {
227                     set += Process.toSdkSandboxUid(uid)
228                 }
229             }
230             return set.toList()
231         }
232     }
233 }
234