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