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