1 /*
2  * Copyright (C) 2019 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.car.settings.datausage;
18 
19 import static android.app.usage.NetworkStats.Bucket.UID_REMOVED;
20 import static android.app.usage.NetworkStats.Bucket.UID_TETHERING;
21 
22 import android.annotation.NonNull;
23 import android.app.usage.NetworkStats;
24 import android.car.drivingstate.CarUxRestrictions;
25 import android.content.Context;
26 import android.content.pm.UserInfo;
27 import android.net.NetworkTemplate;
28 import android.os.UserHandle;
29 import android.util.SparseArray;
30 
31 import androidx.annotation.VisibleForTesting;
32 import androidx.preference.PreferenceGroup;
33 
34 import com.android.car.settings.R;
35 import com.android.car.settings.common.FragmentController;
36 import com.android.car.settings.common.PreferenceController;
37 import com.android.car.settings.common.ProgressBarPreference;
38 import com.android.car.settings.profiles.ProfileHelper;
39 import com.android.settingslib.AppItem;
40 import com.android.settingslib.net.UidDetail;
41 import com.android.settingslib.net.UidDetailProvider;
42 import com.android.settingslib.utils.ThreadUtils;
43 
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.List;
47 import java.util.Optional;
48 
49 import javax.annotation.Nullable;
50 
51 /**
52  * Controller that adds all the applications using the data sorted by the amount of data used. The
53  * first application that used most amount of data will be at the top with progress 100 percentage.
54  * All other progress are calculated relatively.
55  */
56 public class AppDataUsagePreferenceController extends
57         PreferenceController<PreferenceGroup> implements AppsNetworkStatsManager.Callback {
58 
59     private final UidDetailProvider mUidDetailProvider;
60     private NetworkTemplate mNetworkTemplate;
61 
AppDataUsagePreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)62     public AppDataUsagePreferenceController(Context context, String preferenceKey,
63             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
64         this(context, preferenceKey, fragmentController, uxRestrictions,
65                 new UidDetailProvider(context));
66     }
67 
68     @VisibleForTesting
AppDataUsagePreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions, UidDetailProvider uidDetailProvider)69     AppDataUsagePreferenceController(Context context, String preferenceKey,
70             FragmentController fragmentController, CarUxRestrictions uxRestrictions,
71             UidDetailProvider uidDetailProvider) {
72         super(context, preferenceKey, fragmentController, uxRestrictions);
73         mUidDetailProvider = uidDetailProvider;
74     }
75 
76     @VisibleForTesting
hasNextBucket(@onNull NetworkStats stats)77     boolean hasNextBucket(@NonNull NetworkStats stats) {
78         return stats.hasNextBucket();
79     }
80 
81     @NonNull
82     @VisibleForTesting
getNextBucket(@onNull NetworkStats stats)83     NetworkStats.Bucket getNextBucket(@NonNull NetworkStats stats) {
84         NetworkStats.Bucket bucket = new NetworkStats.Bucket();
85         stats.getNextBucket(bucket);
86         return bucket;
87     }
88 
89     @Override
getPreferenceType()90     protected Class<PreferenceGroup> getPreferenceType() {
91         return PreferenceGroup.class;
92     }
93 
94     @Override
onDataLoaded(@ullable NetworkStats stats, @Nullable int[] restrictedUids)95     public void onDataLoaded(@Nullable NetworkStats stats, @Nullable int[] restrictedUids) {
96         List<AppItem> items = new ArrayList<>();
97         long largest = 0;
98 
99         List<UserInfo> profiles = ProfileHelper.getInstance(getContext()).getAllProfiles();
100         SparseArray<AppItem> knownItems = new SparseArray<>();
101 
102         if (stats != null) {
103             while (hasNextBucket(stats)) {
104                 NetworkStats.Bucket entry = getNextBucket(stats);
105                 long size = aggregateDataUsage(knownItems, items, entry, profiles);
106                 largest = Math.max(size, largest);
107             }
108         }
109 
110         updateRestrictedState(restrictedUids, knownItems, items, profiles);
111         sortAndAddPreferences(items, largest);
112     }
113 
114     /** Sets the {@link NetworkTemplate}  */
setNetworkTemplate(NetworkTemplate networkTemplate)115     public void setNetworkTemplate(NetworkTemplate networkTemplate) {
116         mNetworkTemplate = networkTemplate;
117     }
118 
aggregateDataUsage(SparseArray<AppItem> knownItems, List<AppItem> items, NetworkStats.Bucket entry, List<UserInfo> profiles)119     private long aggregateDataUsage(SparseArray<AppItem> knownItems, List<AppItem> items,
120             NetworkStats.Bucket entry, List<UserInfo> profiles) {
121         int currentUserId = UserHandle.myUserId();
122 
123         // Decide how to collapse items together.
124         int uid = entry.getUid();
125 
126         int collapseKey;
127         int category;
128         int userId = UserHandle.getUserId(uid);
129 
130         if (isUidValid(uid)) {
131             collapseKey = uid;
132             category = AppItem.CATEGORY_APP;
133             return accumulate(collapseKey, knownItems, entry, category, items);
134         }
135 
136         if (!UserHandle.isApp(uid)) {
137             collapseKey = android.os.Process.SYSTEM_UID;
138             category = AppItem.CATEGORY_APP;
139             return accumulate(collapseKey, knownItems, entry, category, items);
140         }
141 
142         if (profileContainsUserId(profiles, userId) && userId == currentUserId) {
143             // Add to app item.
144             collapseKey = uid;
145             category = AppItem.CATEGORY_APP;
146             return accumulate(collapseKey, knownItems, entry, category, items);
147         }
148 
149         if (profileContainsUserId(profiles, userId) && userId != currentUserId) {
150             // Add to a managed user item.
151             int managedKey = UidDetailProvider.buildKeyForUser(userId);
152             long usersLargest = accumulate(managedKey, knownItems, entry, AppItem.CATEGORY_USER,
153                     items);
154             collapseKey = uid;
155             category = AppItem.CATEGORY_APP;
156             long appLargest = accumulate(collapseKey, knownItems, entry, category, items);
157             return Math.max(usersLargest, appLargest);
158         }
159 
160         // If it is a removed user add it to the removed users' key.
161         Optional<UserInfo> info = profiles.stream().filter(
162                 userInfo -> userInfo.id == userId).findFirst();
163         if (!info.isPresent()) {
164             collapseKey = UID_REMOVED;
165             category = AppItem.CATEGORY_APP;
166         } else {
167             // Add to other user item.
168             collapseKey = UidDetailProvider.buildKeyForUser(userId);
169             category = AppItem.CATEGORY_USER;
170         }
171 
172         return accumulate(collapseKey, knownItems, entry, category, items);
173     }
174 
175     /**
176      * UID does not belong to a regular app and maybe belongs to a removed application or
177      * application using for tethering traffic.
178      */
isUidValid(int uid)179     private boolean isUidValid(int uid) {
180         return !UserHandle.isApp(uid) && (uid == UID_REMOVED || uid == UID_TETHERING);
181     }
182 
profileContainsUserId(List<UserInfo> profiles, int userId)183     private boolean profileContainsUserId(List<UserInfo> profiles, int userId) {
184         return profiles.stream().anyMatch(userInfo -> userInfo.id == userId);
185     }
186 
updateRestrictedState(@ullable int[] restrictedUids, SparseArray<AppItem> knownItems, List<AppItem> items, List<UserInfo> profiles)187     private void updateRestrictedState(@Nullable int[] restrictedUids,
188             SparseArray<AppItem> knownItems, List<AppItem> items, List<UserInfo> profiles) {
189         if (restrictedUids == null) {
190             return;
191         }
192 
193         for (int i = 0; i < restrictedUids.length; ++i) {
194             int uid = restrictedUids[i];
195             // Only splice in restricted state for current user or managed users.
196             if (!profileContainsUserId(profiles, uid)) {
197                 continue;
198             }
199 
200             AppItem item = knownItems.get(uid);
201             if (item == null) {
202                 item = new AppItem(uid);
203                 item.total = -1;
204                 items.add(item);
205                 knownItems.put(item.key, item);
206             }
207             item.restricted = true;
208         }
209     }
210 
sortAndAddPreferences(List<AppItem> items, long largest)211     private void sortAndAddPreferences(List<AppItem> items, long largest) {
212         getPreference().removeAll();
213         Collections.sort(items);
214         for (int i = 0; i < items.size(); i++) {
215             int percentTotal = largest != 0 ? (int) (items.get(i).total * 100 / largest) : 0;
216             AppDataUsagePreference preference = new AppDataUsagePreference(getContext(),
217                     items.get(i), percentTotal, mUidDetailProvider);
218             getPreference().addPreference(preference);
219         }
220     }
221 
222     /**
223      * Accumulate data usage of a network stats entry for the item mapped by the collapse key.
224      * Creates the item if needed.
225      *
226      * @param collapseKey  the collapse key used to map the item.
227      * @param knownItems   collection of known (already existing) items.
228      * @param entry        the network stats entry to extract data usage from.
229      * @param itemCategory the item is categorized on the list view by this category. Must be
230      */
accumulate(int collapseKey, SparseArray<AppItem> knownItems, NetworkStats.Bucket entry, int itemCategory, List<AppItem> items)231     private static long accumulate(int collapseKey, SparseArray<AppItem> knownItems,
232             NetworkStats.Bucket entry, int itemCategory, List<AppItem> items) {
233         int uid = entry.getUid();
234         AppItem item = knownItems.get(collapseKey);
235         if (item == null) {
236             item = new AppItem(collapseKey);
237             item.category = itemCategory;
238             items.add(item);
239             knownItems.put(item.key, item);
240         }
241         item.addUid(uid);
242         item.total += entry.getRxBytes() + entry.getTxBytes();
243         return item.total;
244     }
245 
246     private class AppDataUsagePreference extends ProgressBarPreference {
247 
248         private final AppItem mItem;
249         private final int mPercent;
250         private UidDetail mDetail;
251 
AppDataUsagePreference(Context context, AppItem item, int percent, UidDetailProvider provider)252         AppDataUsagePreference(Context context, AppItem item, int percent,
253                 UidDetailProvider provider) {
254             super(context);
255             mItem = item;
256             mPercent = percent;
257             setLayoutResource(R.layout.progress_bar_preference);
258             setKey(String.valueOf(item.key));
259             if (item.restricted && item.total <= 0) {
260                 setSummary(R.string.data_usage_app_restricted);
261             } else {
262                 CharSequence s = DataUsageUtils.bytesToIecUnits(context, item.total);
263                 setSummary(s);
264             }
265             mDetail = provider.getUidDetail(item.key, /* blocking= */ false);
266             if (mDetail != null) {
267                 setAppInfo();
268                 setOnClickListener();
269             } else {
270                 ThreadUtils.postOnBackgroundThread(() -> {
271                     mDetail = provider.getUidDetail(mItem.key, /* blocking= */ true);
272                     ThreadUtils.postOnMainThread(() -> {
273                         setAppInfo();
274                         setOnClickListener();
275                     });
276                 });
277             }
278         }
279 
setAppInfo()280         private void setAppInfo() {
281             if (mDetail != null) {
282                 setIcon(mDetail.icon);
283                 setTitle(mDetail.label);
284                 setProgress(mPercent);
285             } else {
286                 setIcon(null);
287                 setTitle(null);
288             }
289         }
290 
setOnClickListener()291         private void setOnClickListener() {
292             if (mDetail != null && mNetworkTemplate != null) {
293                 setOnPreferenceClickListener(p -> {
294                     getFragmentController().launchFragment(
295                             AppSpecificDataUsageFragment.getInstance(mItem, mNetworkTemplate));
296                     return true;
297                 });
298             }
299         }
300     }
301 }
302