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