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