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