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