1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.settings.datausage;
16 
17 import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND;
18 import static android.net.NetworkStatsHistory.FIELD_RX_BYTES;
19 import static android.net.NetworkStatsHistory.FIELD_TX_BYTES;
20 import static android.net.TrafficStats.UID_REMOVED;
21 import static android.net.TrafficStats.UID_TETHERING;
22 
23 import android.app.Activity;
24 import android.app.ActivityManager;
25 import android.app.settings.SettingsEnums;
26 import android.app.usage.NetworkStats;
27 import android.app.usage.NetworkStats.Bucket;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.pm.UserInfo;
31 import android.graphics.Color;
32 import android.net.ConnectivityManager;
33 import android.net.NetworkPolicy;
34 import android.net.NetworkTemplate;
35 import android.os.Bundle;
36 import android.os.Process;
37 import android.os.UserHandle;
38 import android.os.UserManager;
39 import android.provider.Settings;
40 import android.telephony.SubscriptionInfo;
41 import android.telephony.SubscriptionManager;
42 import android.util.Log;
43 import android.util.SparseArray;
44 import android.view.View;
45 import android.view.View.AccessibilityDelegate;
46 import android.view.accessibility.AccessibilityEvent;
47 import android.widget.AdapterView;
48 import android.widget.AdapterView.OnItemSelectedListener;
49 import android.widget.ImageView;
50 import android.widget.Spinner;
51 
52 import androidx.annotation.VisibleForTesting;
53 import androidx.loader.app.LoaderManager.LoaderCallbacks;
54 import androidx.loader.content.Loader;
55 import androidx.preference.Preference;
56 import androidx.preference.PreferenceGroup;
57 
58 import com.android.settings.R;
59 import com.android.settings.core.SubSettingLauncher;
60 import com.android.settings.datausage.CycleAdapter.SpinnerInterface;
61 import com.android.settings.network.MobileDataEnabledListener;
62 import com.android.settings.network.ProxySubscriptionManager;
63 import com.android.settings.widget.LoadingViewController;
64 import com.android.settingslib.AppItem;
65 import com.android.settingslib.net.NetworkCycleChartData;
66 import com.android.settingslib.net.NetworkCycleChartDataLoader;
67 import com.android.settingslib.net.NetworkStatsSummaryLoader;
68 import com.android.settingslib.net.UidDetailProvider;
69 
70 import java.util.ArrayList;
71 import java.util.Collections;
72 import java.util.List;
73 
74 /**
75  * Panel showing data usage history across various networks, including options
76  * to inspect based on usage cycle and control through {@link NetworkPolicy}.
77  */
78 public class DataUsageList extends DataUsageBaseFragment
79         implements MobileDataEnabledListener.Client {
80 
81     static final String EXTRA_SUB_ID = "sub_id";
82     static final String EXTRA_NETWORK_TEMPLATE = "network_template";
83     static final String EXTRA_NETWORK_TYPE = "network_type";
84 
85     private static final String TAG = "DataUsageList";
86     private static final boolean LOGD = false;
87 
88     private static final String KEY_USAGE_AMOUNT = "usage_amount";
89     private static final String KEY_CHART_DATA = "chart_data";
90     private static final String KEY_APPS_GROUP = "apps_group";
91     private static final String KEY_TEMPLATE = "template";
92     private static final String KEY_APP = "app";
93     private static final String KEY_FIELDS = "fields";
94 
95     @VisibleForTesting
96     static final int LOADER_CHART_DATA = 2;
97     @VisibleForTesting
98     static final int LOADER_SUMMARY = 3;
99 
100     @VisibleForTesting
101     MobileDataEnabledListener mDataStateListener;
102 
103     @VisibleForTesting
104     NetworkTemplate mTemplate;
105     @VisibleForTesting
106     int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
107     @VisibleForTesting
108     int mNetworkType;
109     @VisibleForTesting
110     Spinner mCycleSpinner;
111     @VisibleForTesting
112     LoadingViewController mLoadingViewController;
113 
114     private ChartDataUsagePreference mChart;
115     private List<NetworkCycleChartData> mCycleData;
116     private ArrayList<Long> mCycles;
117     private UidDetailProvider mUidDetailProvider;
118     private CycleAdapter mCycleAdapter;
119     private Preference mUsageAmount;
120     private PreferenceGroup mApps;
121     private View mHeader;
122 
123     @Override
getMetricsCategory()124     public int getMetricsCategory() {
125         return SettingsEnums.DATA_USAGE_LIST;
126     }
127 
128     @Override
onCreate(Bundle savedInstanceState)129     public void onCreate(Bundle savedInstanceState) {
130         super.onCreate(savedInstanceState);
131         final Activity activity = getActivity();
132 
133         if (!isBandwidthControlEnabled()) {
134             Log.w(TAG, "No bandwidth control; leaving");
135             activity.finish();
136             return;
137         }
138 
139         mUidDetailProvider = new UidDetailProvider(activity);
140         mUsageAmount = findPreference(KEY_USAGE_AMOUNT);
141         mChart = findPreference(KEY_CHART_DATA);
142         mApps = findPreference(KEY_APPS_GROUP);
143         processArgument();
144         mDataStateListener = new MobileDataEnabledListener(activity, this);
145     }
146 
147     @Override
onViewCreated(View v, Bundle savedInstanceState)148     public void onViewCreated(View v, Bundle savedInstanceState) {
149         super.onViewCreated(v, savedInstanceState);
150 
151         mHeader = setPinnedHeaderView(R.layout.apps_filter_spinner);
152         mHeader.findViewById(R.id.filter_settings).setOnClickListener(btn -> {
153             final Bundle args = new Bundle();
154             args.putParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE, mTemplate);
155             new SubSettingLauncher(getContext())
156                     .setDestination(BillingCycleSettings.class.getName())
157                     .setTitleRes(R.string.billing_cycle)
158                     .setSourceMetricsCategory(getMetricsCategory())
159                     .setArguments(args)
160                     .launch();
161         });
162         mCycleSpinner = mHeader.findViewById(R.id.filter_spinner);
163         mCycleSpinner.setVisibility(View.GONE);
164         mCycleAdapter = new CycleAdapter(mCycleSpinner.getContext(), new SpinnerInterface() {
165             @Override
166             public void setAdapter(CycleAdapter cycleAdapter) {
167                 mCycleSpinner.setAdapter(cycleAdapter);
168             }
169 
170             @Override
171             public void setOnItemSelectedListener(OnItemSelectedListener listener) {
172                 mCycleSpinner.setOnItemSelectedListener(listener);
173             }
174 
175             @Override
176             public Object getSelectedItem() {
177                 return mCycleSpinner.getSelectedItem();
178             }
179 
180             @Override
181             public void setSelection(int position) {
182                 mCycleSpinner.setSelection(position);
183             }
184         }, mCycleListener);
185         mCycleSpinner.setAccessibilityDelegate(new AccessibilityDelegate() {
186             @Override
187             public void sendAccessibilityEvent(View host, int eventType) {
188                 if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
189                     // Ignore TYPE_VIEW_SELECTED or TalkBack will speak for it at onResume.
190                     return;
191                 }
192                 super.sendAccessibilityEvent(host, eventType);
193             }
194         });
195 
196         mLoadingViewController = new LoadingViewController(
197                 getView().findViewById(R.id.loading_container), getListView());
198         mLoadingViewController.showLoadingViewDelayed();
199     }
200 
201     @Override
onResume()202     public void onResume() {
203         super.onResume();
204         mDataStateListener.start(mSubId);
205 
206         // kick off loader for network history
207         // TODO: consider chaining two loaders together instead of reloading
208         // network history when showing app detail.
209         getLoaderManager().restartLoader(LOADER_CHART_DATA,
210                 buildArgs(mTemplate), mNetworkCycleDataCallbacks);
211 
212         updateBody();
213     }
214 
215     @Override
onPause()216     public void onPause() {
217         super.onPause();
218         mDataStateListener.stop();
219 
220         getLoaderManager().destroyLoader(LOADER_CHART_DATA);
221         getLoaderManager().destroyLoader(LOADER_SUMMARY);
222     }
223 
224     @Override
onDestroy()225     public void onDestroy() {
226         mUidDetailProvider.clearCache();
227         mUidDetailProvider = null;
228 
229         super.onDestroy();
230     }
231 
232     @Override
getPreferenceScreenResId()233     protected int getPreferenceScreenResId() {
234         return R.xml.data_usage_list;
235     }
236 
237     @Override
getLogTag()238     protected String getLogTag() {
239         return TAG;
240     }
241 
processArgument()242     void processArgument() {
243         final Bundle args = getArguments();
244         if (args != null) {
245             mSubId = args.getInt(EXTRA_SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID);
246             mTemplate = args.getParcelable(EXTRA_NETWORK_TEMPLATE);
247             mNetworkType = args.getInt(EXTRA_NETWORK_TYPE, ConnectivityManager.TYPE_MOBILE);
248         }
249         if (mTemplate == null && mSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
250             final Intent intent = getIntent();
251             mSubId = intent.getIntExtra(Settings.EXTRA_SUB_ID,
252                     SubscriptionManager.INVALID_SUBSCRIPTION_ID);
253             mTemplate = intent.getParcelableExtra(Settings.EXTRA_NETWORK_TEMPLATE);
254         }
255     }
256 
257     /**
258      * Implementation of {@code MobileDataEnabledListener.Client}
259      */
onMobileDataEnabledChange()260     public void onMobileDataEnabledChange() {
261         updatePolicy();
262     }
263 
264     /**
265      * Update body content based on current tab. Loads network cycle data from system, and
266      * binds them to visible controls.
267      */
updateBody()268     private void updateBody() {
269         if (!isAdded()) return;
270 
271         final Context context = getActivity();
272 
273         // detail mode can change visible menus, invalidate
274         getActivity().invalidateOptionsMenu();
275 
276         int seriesColor = context.getColor(R.color.sim_noitification);
277         if (mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
278             final SubscriptionInfo sir = ProxySubscriptionManager.getInstance(context)
279                     .getActiveSubscriptionInfo(mSubId);
280 
281             if (sir != null) {
282                 seriesColor = sir.getIconTint();
283             }
284         }
285 
286         final int secondaryColor = Color.argb(127, Color.red(seriesColor), Color.green(seriesColor),
287                 Color.blue(seriesColor));
288         mChart.setColors(seriesColor, secondaryColor);
289     }
290 
buildArgs(NetworkTemplate template)291     private Bundle buildArgs(NetworkTemplate template) {
292         final Bundle args = new Bundle();
293         args.putParcelable(KEY_TEMPLATE, template);
294         args.putParcelable(KEY_APP, null);
295         args.putInt(KEY_FIELDS, FIELD_RX_BYTES | FIELD_TX_BYTES);
296         return args;
297     }
298 
299     /**
300      * Update chart sweeps and cycle list to reflect {@link NetworkPolicy} for
301      * current {@link #mTemplate}.
302      */
303     @VisibleForTesting
updatePolicy()304     void updatePolicy() {
305         final NetworkPolicy policy = services.mPolicyEditor.getPolicy(mTemplate);
306         final View configureButton = mHeader.findViewById(R.id.filter_settings);
307         //SUB SELECT
308         if (isNetworkPolicyModifiable(policy, mSubId) && isMobileDataAvailable(mSubId)) {
309             mChart.setNetworkPolicy(policy);
310             configureButton.setVisibility(View.VISIBLE);
311             ((ImageView) configureButton).setColorFilter(android.R.color.white);
312         } else {
313             // controls are disabled; don't bind warning/limit sweeps
314             mChart.setNetworkPolicy(null);
315             configureButton.setVisibility(View.GONE);
316         }
317 
318         // generate cycle list based on policy and available history
319         if (mCycleAdapter.updateCycleList(mCycleData)) {
320             updateDetailData();
321         }
322     }
323 
324     /**
325      * Update details based on {@link #mChart} inspection range depending on
326      * current mode. Updates {@link #mAdapter} with sorted list
327      * of applications data usage.
328      */
updateDetailData()329     private void updateDetailData() {
330         if (LOGD) Log.d(TAG, "updateDetailData()");
331 
332         // kick off loader for detailed stats
333         getLoaderManager().restartLoader(LOADER_SUMMARY, null /* args */,
334                 mNetworkStatsDetailCallbacks);
335 
336         final long totalBytes = mCycleData != null && !mCycleData.isEmpty()
337             ? mCycleData.get(mCycleSpinner.getSelectedItemPosition()).getTotalUsage() : 0;
338         final CharSequence totalPhrase = DataUsageUtils.formatDataUsage(getActivity(), totalBytes);
339         mUsageAmount.setTitle(getString(R.string.data_used_template, totalPhrase));
340     }
341 
342     /**
343      * Bind the given {@link NetworkStats}, or {@code null} to clear list.
344      */
bindStats(NetworkStats stats, int[] restrictedUids)345     private void bindStats(NetworkStats stats, int[] restrictedUids) {
346         mApps.removeAll();
347         if (stats == null) {
348             if (LOGD) {
349                 Log.d(TAG, "No network stats data. App list cleared.");
350             }
351             return;
352         }
353 
354         final ArrayList<AppItem> items = new ArrayList<>();
355         long largest = 0;
356 
357         final int currentUserId = ActivityManager.getCurrentUser();
358         final UserManager userManager = UserManager.get(getContext());
359         final List<UserHandle> profiles = userManager.getUserProfiles();
360         final SparseArray<AppItem> knownItems = new SparseArray<AppItem>();
361 
362         final Bucket bucket = new Bucket();
363         while (stats.hasNextBucket() && stats.getNextBucket(bucket)) {
364             // Decide how to collapse items together
365             final int uid = bucket.getUid();
366             final int collapseKey;
367             final int category;
368             final int userId = UserHandle.getUserId(uid);
369             if (UserHandle.isApp(uid)) {
370                 if (profiles.contains(new UserHandle(userId))) {
371                     if (userId != currentUserId) {
372                         // Add to a managed user item.
373                         final int managedKey = UidDetailProvider.buildKeyForUser(userId);
374                         largest = accumulate(managedKey, knownItems, bucket,
375                             AppItem.CATEGORY_USER, items, largest);
376                     }
377                     // Add to app item.
378                     collapseKey = uid;
379                     category = AppItem.CATEGORY_APP;
380                 } else {
381                     // If it is a removed user add it to the removed users' key
382                     final UserInfo info = userManager.getUserInfo(userId);
383                     if (info == null) {
384                         collapseKey = UID_REMOVED;
385                         category = AppItem.CATEGORY_APP;
386                     } else {
387                         // Add to other user item.
388                         collapseKey = UidDetailProvider.buildKeyForUser(userId);
389                         category = AppItem.CATEGORY_USER;
390                     }
391                 }
392             } else if (uid == UID_REMOVED || uid == UID_TETHERING
393                     || uid == Process.OTA_UPDATE_UID) {
394                 collapseKey = uid;
395                 category = AppItem.CATEGORY_APP;
396             } else {
397                 collapseKey = android.os.Process.SYSTEM_UID;
398                 category = AppItem.CATEGORY_APP;
399             }
400             largest = accumulate(collapseKey, knownItems, bucket, category, items, largest);
401         }
402         stats.close();
403 
404         final int restrictedUidsMax = restrictedUids.length;
405         for (int i = 0; i < restrictedUidsMax; ++i) {
406             final int uid = restrictedUids[i];
407             // Only splice in restricted state for current user or managed users
408             if (!profiles.contains(new UserHandle(UserHandle.getUserId(uid)))) {
409                 continue;
410             }
411 
412             AppItem item = knownItems.get(uid);
413             if (item == null) {
414                 item = new AppItem(uid);
415                 item.total = -1;
416                 items.add(item);
417                 knownItems.put(item.key, item);
418             }
419             item.restricted = true;
420         }
421 
422         Collections.sort(items);
423         for (int i = 0; i < items.size(); i++) {
424             final int percentTotal = largest != 0 ? (int) (items.get(i).total * 100 / largest) : 0;
425             final AppDataUsagePreference preference = new AppDataUsagePreference(getContext(),
426                     items.get(i), percentTotal, mUidDetailProvider);
427             preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
428                 @Override
429                 public boolean onPreferenceClick(Preference preference) {
430                     AppDataUsagePreference pref = (AppDataUsagePreference) preference;
431                     AppItem item = pref.getItem();
432                     startAppDataUsage(item);
433                     return true;
434                 }
435             });
436             mApps.addPreference(preference);
437         }
438     }
439 
440     @VisibleForTesting
startAppDataUsage(AppItem item)441     void startAppDataUsage(AppItem item) {
442         final Bundle args = new Bundle();
443         args.putParcelable(AppDataUsage.ARG_APP_ITEM, item);
444         args.putParcelable(AppDataUsage.ARG_NETWORK_TEMPLATE, mTemplate);
445         if (mCycles == null) {
446             mCycles = new ArrayList<>();
447             for (NetworkCycleChartData data : mCycleData) {
448                 if (mCycles.isEmpty()) {
449                     mCycles.add(data.getEndTime());
450                 }
451                 mCycles.add(data.getStartTime());
452             }
453         }
454         args.putSerializable(AppDataUsage.ARG_NETWORK_CYCLES, mCycles);
455         args.putLong(AppDataUsage.ARG_SELECTED_CYCLE,
456             mCycleData.get(mCycleSpinner.getSelectedItemPosition()).getEndTime());
457 
458         new SubSettingLauncher(getContext())
459                 .setDestination(AppDataUsage.class.getName())
460                 .setTitleRes(R.string.data_usage_app_summary_title)
461                 .setArguments(args)
462                 .setSourceMetricsCategory(getMetricsCategory())
463                 .launch();
464     }
465 
466     /**
467      * Accumulate data usage of a network stats entry for the item mapped by the collapse key.
468      * Creates the item if needed.
469      *
470      * @param collapseKey  the collapse key used to map the item.
471      * @param knownItems   collection of known (already existing) items.
472      * @param bucket       the network stats bucket to extract data usage from.
473      * @param itemCategory the item is categorized on the list view by this category. Must be
474      */
accumulate(int collapseKey, final SparseArray<AppItem> knownItems, Bucket bucket, int itemCategory, ArrayList<AppItem> items, long largest)475     private static long accumulate(int collapseKey, final SparseArray<AppItem> knownItems,
476             Bucket bucket, int itemCategory, ArrayList<AppItem> items, long largest) {
477         final int uid = bucket.getUid();
478         AppItem item = knownItems.get(collapseKey);
479         if (item == null) {
480             item = new AppItem(collapseKey);
481             item.category = itemCategory;
482             items.add(item);
483             knownItems.put(item.key, item);
484         }
485         item.addUid(uid);
486         item.total += bucket.getRxBytes() + bucket.getTxBytes();
487         return Math.max(largest, item.total);
488     }
489 
490     private OnItemSelectedListener mCycleListener = new OnItemSelectedListener() {
491         @Override
492         public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
493             final CycleAdapter.CycleItem cycle = (CycleAdapter.CycleItem)
494                     mCycleSpinner.getSelectedItem();
495 
496             if (LOGD) {
497                 Log.d(TAG, "showing cycle " + cycle + ", start=" + cycle.start + ", end="
498                         + cycle.end + "]");
499             }
500 
501             // update chart to show selected cycle, and update detail data
502             // to match updated sweep bounds.
503             mChart.setNetworkCycleData(mCycleData.get(position));
504 
505             updateDetailData();
506         }
507 
508         @Override
509         public void onNothingSelected(AdapterView<?> parent) {
510             // ignored
511         }
512     };
513 
514     @VisibleForTesting
515     final LoaderCallbacks<List<NetworkCycleChartData>> mNetworkCycleDataCallbacks =
516             new LoaderCallbacks<List<NetworkCycleChartData>>() {
517         @Override
518         public Loader<List<NetworkCycleChartData>> onCreateLoader(int id, Bundle args) {
519             return NetworkCycleChartDataLoader.builder(getContext())
520                     .setNetworkTemplate(mTemplate)
521                     .build();
522         }
523 
524         @Override
525         public void onLoadFinished(Loader<List<NetworkCycleChartData>> loader,
526                 List<NetworkCycleChartData> data) {
527             mLoadingViewController.showContent(false /* animate */);
528             mCycleData = data;
529             // calculate policy cycles based on available data
530             updatePolicy();
531             mCycleSpinner.setVisibility(View.VISIBLE);
532         }
533 
534         @Override
535         public void onLoaderReset(Loader<List<NetworkCycleChartData>> loader) {
536             mCycleData = null;
537         }
538     };
539 
540     private final LoaderCallbacks<NetworkStats> mNetworkStatsDetailCallbacks =
541             new LoaderCallbacks<NetworkStats>() {
542         @Override
543         public Loader<NetworkStats> onCreateLoader(int id, Bundle args) {
544             return new NetworkStatsSummaryLoader.Builder(getContext())
545                     .setStartTime(mChart.getInspectStart())
546                     .setEndTime(mChart.getInspectEnd())
547                     .setNetworkTemplate(mTemplate)
548                     .build();
549         }
550 
551         @Override
552         public void onLoadFinished(Loader<NetworkStats> loader, NetworkStats data) {
553             final int[] restrictedUids = services.mPolicyManager.getUidsWithPolicy(
554                     POLICY_REJECT_METERED_BACKGROUND);
555             bindStats(data, restrictedUids);
556             updateEmptyVisible();
557         }
558 
559         @Override
560         public void onLoaderReset(Loader<NetworkStats> loader) {
561             bindStats(null, new int[0]);
562             updateEmptyVisible();
563         }
564 
565         private void updateEmptyVisible() {
566             if ((mApps.getPreferenceCount() != 0) !=
567                     (getPreferenceScreen().getPreferenceCount() != 0)) {
568                 if (mApps.getPreferenceCount() != 0) {
569                     getPreferenceScreen().addPreference(mUsageAmount);
570                     getPreferenceScreen().addPreference(mApps);
571                 } else {
572                     getPreferenceScreen().removeAll();
573                 }
574             }
575         }
576     };
577 }
578