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