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