Files
packages_apps_Settings/src/com/android/settings/DataUsageSummary.java
Jeff Sharkey 94a9095eac Read network policy earlier, fix ugly scrollbar.
Read policy before creating options menu, so we correctly populate
checkboxes.  Also clean up Preference views so we don't crash when
returning from app details.

Change-Id: I52c38a3016ea669e27b325bfb6ae1620d2a5ca53
2011-06-13 22:31:09 -07:00

701 lines
25 KiB
Java

/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings;
import static android.net.NetworkPolicy.LIMIT_DISABLED;
import static android.net.NetworkPolicyManager.computeLastCycleBoundary;
import static android.net.NetworkPolicyManager.computeNextCycleBoundary;
import static android.net.TrafficStats.TEMPLATE_MOBILE_3G_LOWER;
import static android.net.TrafficStats.TEMPLATE_MOBILE_4G;
import static android.net.TrafficStats.TEMPLATE_MOBILE_ALL;
import static android.net.TrafficStats.TEMPLATE_WIFI;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.INetworkPolicyManager;
import android.net.INetworkStatsService;
import android.net.NetworkPolicy;
import android.net.NetworkStats;
import android.net.NetworkStatsHistory;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.SwitchPreference;
import android.telephony.TelephonyManager;
import android.text.format.DateUtils;
import android.text.format.Formatter;
import android.text.format.Time;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.Spinner;
import android.widget.TabHost;
import android.widget.TabHost.OnTabChangeListener;
import android.widget.TabHost.TabContentFactory;
import android.widget.TabHost.TabSpec;
import android.widget.TabWidget;
import android.widget.TextView;
import com.android.settings.net.NetworkPolicyModifier;
import com.android.settings.widget.DataUsageChartView;
import com.android.settings.widget.DataUsageChartView.DataUsageChartListener;
import com.google.android.collect.Lists;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
public class DataUsageSummary extends Fragment {
private static final String TAG = "DataUsage";
private static final boolean LOGD = true;
private static final int TEMPLATE_INVALID = -1;
private static final String TAB_3G = "3g";
private static final String TAB_4G = "4g";
private static final String TAB_MOBILE = "mobile";
private static final String TAB_WIFI = "wifi";
private static final long KB_IN_BYTES = 1024;
private static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
private static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
private INetworkStatsService mStatsService;
private INetworkPolicyManager mPolicyService;
private TabHost mTabHost;
private TabWidget mTabWidget;
private ListView mListView;
private DataUsageAdapter mAdapter;
private View mHeader;
private LinearLayout mSwitches;
private SwitchPreference mDataEnabled;
private CheckBoxPreference mDisableAtLimit;
private View mDataEnabledView;
private View mDisableAtLimitView;
private DataUsageChartView mChart;
private Spinner mCycleSpinner;
private CycleAdapter mCycleAdapter;
// TODO: persist show wifi flag
private boolean mShowWifi = false;
private int mTemplate = TEMPLATE_INVALID;
private NetworkPolicyModifier mPolicyModifier;
private NetworkStatsHistory mHistory;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mStatsService = INetworkStatsService.Stub.asInterface(
ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
mPolicyService = INetworkPolicyManager.Stub.asInterface(
ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
final Context context = getActivity();
final String subscriberId = getActiveSubscriberId(context);
mPolicyModifier = new NetworkPolicyModifier(mPolicyService, subscriberId);
mPolicyModifier.read();
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final Context context = inflater.getContext();
final View view = inflater.inflate(R.layout.data_usage_summary, container, false);
mTabHost = (TabHost) view.findViewById(android.R.id.tabhost);
mTabWidget = (TabWidget) view.findViewById(android.R.id.tabs);
mListView = (ListView) view.findViewById(android.R.id.list);
mTabHost.setup();
mTabHost.setOnTabChangedListener(mTabListener);
mHeader = inflater.inflate(R.layout.data_usage_header, mListView, false);
mListView.addHeaderView(mHeader, null, false);
mDataEnabled = new SwitchPreference(context);
mDisableAtLimit = new CheckBoxPreference(context);
// kick refresh once to force-create views
refreshPreferenceViews();
// TODO: remove once thin preferences are supported (48dip)
mDataEnabledView.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, 72));
mDisableAtLimitView.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, 72));
mDataEnabledView.setOnClickListener(mDataEnabledListener);
mDisableAtLimitView.setOnClickListener(mDisableAtLimitListener);
mSwitches = (LinearLayout) mHeader.findViewById(R.id.switches);
mSwitches.addView(mDataEnabledView);
mSwitches.addView(mDisableAtLimitView);
mCycleSpinner = (Spinner) mHeader.findViewById(R.id.cycles);
mCycleAdapter = new CycleAdapter(context);
mCycleSpinner.setAdapter(mCycleAdapter);
mCycleSpinner.setOnItemSelectedListener(mCycleListener);
mChart = new DataUsageChartView(context);
mChart.setListener(mChartListener);
mChart.setLayoutParams(new AbsListView.LayoutParams(MATCH_PARENT, 350));
mListView.addHeaderView(mChart, null, false);
mAdapter = new DataUsageAdapter();
mListView.setOnItemClickListener(mListListener);
mListView.setAdapter(mAdapter);
return view;
}
@Override
public void onResume() {
super.onResume();
// this kicks off chain reaction which creates tabs, binds the body to
// selected network, and binds chart, cycles and detail list.
updateTabs();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.data_usage, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
final MenuItem split4g = menu.findItem(R.id.action_split_4g);
split4g.setChecked(mPolicyModifier.isMobilePolicySplit());
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_split_4g: {
final boolean mobileSplit = !item.isChecked();
mPolicyModifier.setMobilePolicySplit(mobileSplit);
item.setChecked(mPolicyModifier.isMobilePolicySplit());
updateTabs();
return true;
}
case R.id.action_show_wifi: {
mShowWifi = !item.isChecked();
item.setChecked(mShowWifi);
updateTabs();
return true;
}
}
return false;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mDataEnabledView = null;
mDisableAtLimitView = null;
}
/**
* Rebuild all tabs based on {@link NetworkPolicyModifier} and
* {@link #mShowWifi}, hiding the tabs entirely when applicable. Selects
* first tab, and kicks off a full rebind of body contents.
*/
private void updateTabs() {
final boolean mobileSplit = mPolicyModifier.isMobilePolicySplit();
final boolean tabsVisible = mobileSplit || mShowWifi;
mTabWidget.setVisibility(tabsVisible ? View.VISIBLE : View.GONE);
mTabHost.clearAllTabs();
if (mobileSplit) {
mTabHost.addTab(buildTabSpec(TAB_3G, R.string.data_usage_tab_3g));
mTabHost.addTab(buildTabSpec(TAB_4G, R.string.data_usage_tab_4g));
}
if (mShowWifi) {
if (!mobileSplit) {
mTabHost.addTab(buildTabSpec(TAB_MOBILE, R.string.data_usage_tab_mobile));
}
mTabHost.addTab(buildTabSpec(TAB_WIFI, R.string.data_usage_tab_wifi));
}
if (mTabWidget.getTabCount() > 0) {
// select first tab, which will kick off updateBody()
mTabHost.setCurrentTab(0);
} else {
// no tabs shown; update body manually
updateBody();
}
}
/**
* Factory that provide empty {@link View} to make {@link TabHost} happy.
*/
private TabContentFactory mEmptyTabContent = new TabContentFactory() {
/** {@inheritDoc} */
public View createTabContent(String tag) {
return new View(mTabHost.getContext());
}
};
/**
* Build {@link TabSpec} with thin indicator, and empty content.
*/
private TabSpec buildTabSpec(String tag, int titleRes) {
final LayoutInflater inflater = LayoutInflater.from(mTabWidget.getContext());
final View indicator = inflater.inflate(
R.layout.tab_indicator_thin_holo, mTabWidget, false);
final TextView title = (TextView) indicator.findViewById(android.R.id.title);
title.setText(titleRes);
return mTabHost.newTabSpec(tag).setIndicator(indicator).setContent(mEmptyTabContent);
}
private OnTabChangeListener mTabListener = new OnTabChangeListener() {
/** {@inheritDoc} */
public void onTabChanged(String tabId) {
// user changed tab; update body
updateBody();
}
};
/**
* Update body content based on current tab. Loads
* {@link NetworkStatsHistory} and {@link NetworkPolicy} from system, and
* binds them to visible controls.
*/
private void updateBody() {
final String tabTag = mTabHost.getCurrentTabTag();
final String currentTab = tabTag != null ? tabTag : TAB_MOBILE;
if (LOGD) Log.d(TAG, "updateBody() with currentTab=" + currentTab);
if (TAB_WIFI.equals(currentTab)) {
// wifi doesn't have any controls
mDataEnabledView.setVisibility(View.GONE);
mDisableAtLimitView.setVisibility(View.GONE);
mTemplate = TEMPLATE_WIFI;
} else {
// make sure we show for non-wifi
mDataEnabledView.setVisibility(View.VISIBLE);
mDisableAtLimitView.setVisibility(View.VISIBLE);
}
if (TAB_MOBILE.equals(currentTab)) {
mDataEnabled.setTitle(R.string.data_usage_enable_mobile);
mDisableAtLimit.setTitle(R.string.data_usage_disable_mobile_limit);
mTemplate = TEMPLATE_MOBILE_ALL;
} else if (TAB_3G.equals(currentTab)) {
mDataEnabled.setTitle(R.string.data_usage_enable_3g);
mDisableAtLimit.setTitle(R.string.data_usage_disable_3g_limit);
mTemplate = TEMPLATE_MOBILE_3G_LOWER;
} else if (TAB_4G.equals(currentTab)) {
mDataEnabled.setTitle(R.string.data_usage_enable_4g);
mDisableAtLimit.setTitle(R.string.data_usage_disable_4g_limit);
mTemplate = TEMPLATE_MOBILE_4G;
}
// TODO: populate checkbox based on radio preferences
mDataEnabled.setChecked(true);
try {
// load stats for current template
mHistory = mStatsService.getHistoryForNetwork(mTemplate);
} catch (RemoteException e) {
// since we can't do much without policy or history, and we don't
// want to leave with half-baked UI, we bail hard.
throw new RuntimeException("problem reading network policy or stats", e);
}
// bind chart to historical stats
mChart.bindNetworkStats(mHistory);
updatePolicy(true);
// force scroll to top of body
mListView.smoothScrollToPosition(0);
// kick preference views so they rebind from changes above
refreshPreferenceViews();
}
/**
* Update chart sweeps and cycle list to reflect {@link NetworkPolicy} for
* current {@link #mTemplate}.
*/
private void updatePolicy(boolean refreshCycle) {
final NetworkPolicy policy = mPolicyModifier.getPolicy(mTemplate);
// reflect policy limit in checkbox
mDisableAtLimit.setChecked(policy != null && policy.limitBytes != LIMIT_DISABLED);
mChart.bindNetworkPolicy(policy);
if (refreshCycle) {
// generate cycle list based on policy and available history
updateCycleList(policy);
}
// kick preference views so they rebind from changes above
refreshPreferenceViews();
}
/**
* Return full time bounds (earliest and latest time recorded) of the given
* {@link NetworkStatsHistory}.
*/
public static long[] getHistoryBounds(NetworkStatsHistory history) {
final long currentTime = System.currentTimeMillis();
long start = currentTime;
long end = currentTime;
if (history.bucketCount > 0) {
start = history.bucketStart[0];
end = history.bucketStart[history.bucketCount - 1];
}
return new long[] { start, end };
}
/**
* Rebuild {@link #mCycleAdapter} based on {@link NetworkPolicy#cycleDay}
* and available {@link NetworkStatsHistory} data. Always selects the newest
* item, updating the inspection range on {@link #mChart}.
*/
private void updateCycleList(NetworkPolicy policy) {
mCycleAdapter.clear();
final Context context = mCycleSpinner.getContext();
final long[] bounds = getHistoryBounds(mHistory);
final long historyStart = bounds[0];
final long historyEnd = bounds[1];
if (policy != null) {
// find the next cycle boundary
long cycleEnd = computeNextCycleBoundary(historyEnd, policy);
int guardCount = 0;
// walk backwards, generating all valid cycle ranges
while (cycleEnd > historyStart) {
final long cycleStart = computeLastCycleBoundary(cycleEnd, policy);
Log.d(TAG, "generating cs=" + cycleStart + " to ce=" + cycleEnd + " waiting for hs="
+ historyStart);
mCycleAdapter.add(new CycleItem(context, cycleStart, cycleEnd));
cycleEnd = cycleStart;
// TODO: remove this guard once we have better testing
if (guardCount++ > 50) {
Log.wtf(TAG, "stuck generating ranges for bounds=" + Arrays.toString(bounds)
+ " and policy=" + policy);
}
}
// one last cycle entry to modify policy cycle day
mCycleAdapter.add(new CycleChangeItem(context));
} else {
// no valid cycle; show all data
// TODO: offer simple ranges like "last week" etc
mCycleAdapter.add(new CycleItem(context, historyStart, historyEnd));
}
// force pick the current cycle (first item)
mCycleSpinner.setSelection(0);
mCycleListener.onItemSelected(mCycleSpinner, null, 0, 0);
}
/**
* Force rebind of hijacked {@link Preference} views.
*/
private void refreshPreferenceViews() {
mDataEnabledView = mDataEnabled.getView(mDataEnabledView, mListView);
mDisableAtLimitView = mDisableAtLimit.getView(mDisableAtLimitView, mListView);
}
private OnClickListener mDataEnabledListener = new OnClickListener() {
/** {@inheritDoc} */
public void onClick(View v) {
mDataEnabled.setChecked(!mDataEnabled.isChecked());
refreshPreferenceViews();
// TODO: wire up to telephony to enable/disable radios
}
};
private OnClickListener mDisableAtLimitListener = new OnClickListener() {
/** {@inheritDoc} */
public void onClick(View v) {
final boolean disableAtLimit = !mDisableAtLimit.isChecked();
mDisableAtLimit.setChecked(disableAtLimit);
refreshPreferenceViews();
// TODO: create policy if none exists
// TODO: show interstitial warning dialog to user
final long limitBytes = disableAtLimit ? 5 * GB_IN_BYTES : LIMIT_DISABLED;
mPolicyModifier.setPolicyLimitBytes(mTemplate, limitBytes);
updatePolicy(false);
}
};
private OnItemClickListener mListListener = new OnItemClickListener() {
/** {@inheritDoc} */
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final AppUsageItem app = (AppUsageItem) parent.getItemAtPosition(position);
final Bundle args = new Bundle();
args.putInt(Intent.EXTRA_UID, app.uid);
final PreferenceActivity activity = (PreferenceActivity) getActivity();
activity.startPreferencePanel(DataUsageAppDetail.class.getName(), args,
R.string.data_usage_summary_title, null, null, 0);
}
};
private OnItemSelectedListener mCycleListener = new OnItemSelectedListener() {
/** {@inheritDoc} */
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
final CycleItem cycle = (CycleItem) parent.getItemAtPosition(position);
if (cycle instanceof CycleChangeItem) {
// TODO: show "define cycle" dialog
// also reset back to first cycle
Log.d(TAG, "CHANGE CYCLE DIALOG!!");
} else {
if (LOGD) Log.d(TAG, "shoiwng cycle " + cycle);
// update chart to show selected cycle, and update detail data
// to match updated sweep bounds.
final long[] bounds = getHistoryBounds(mHistory);
mChart.setVisibleRange(cycle.start, cycle.end, bounds[1]);
updateDetailData();
}
}
/** {@inheritDoc} */
public void onNothingSelected(AdapterView<?> parent) {
// ignored
}
};
/**
* Update {@link #mAdapter} with sorted list of applications data usage,
* based on current inspection from {@link #mChart}.
*/
private void updateDetailData() {
if (LOGD) Log.d(TAG, "updateDetailData()");
try {
final long[] range = mChart.getInspectRange();
final NetworkStats stats = mStatsService.getSummaryForAllUid(
range[0], range[1], mTemplate);
mAdapter.bindStats(stats);
} catch (RemoteException e) {
Log.w(TAG, "problem reading stats");
}
}
private static String getActiveSubscriberId(Context context) {
final TelephonyManager telephony = (TelephonyManager) context.getSystemService(
Context.TELEPHONY_SERVICE);
return telephony.getSubscriberId();
}
private DataUsageChartListener mChartListener = new DataUsageChartListener() {
/** {@inheritDoc} */
public void onInspectRangeChanged() {
if (LOGD) Log.d(TAG, "onInspectRangeChanged()");
updateDetailData();
}
/** {@inheritDoc} */
public void onWarningChanged() {
if (LOGD) Log.d(TAG, "onWarningChanged()");
final long warningBytes = mChart.getWarningBytes();
mPolicyModifier.setPolicyWarningBytes(mTemplate, warningBytes);
updatePolicy(false);
}
/** {@inheritDoc} */
public void onLimitChanged() {
if (LOGD) Log.d(TAG, "onLimitChanged()");
final long limitBytes = mDisableAtLimit.isChecked() ? mChart.getLimitBytes()
: LIMIT_DISABLED;
mPolicyModifier.setPolicyLimitBytes(mTemplate, limitBytes);
updatePolicy(false);
}
};
/**
* List item that reflects a specific data usage cycle.
*/
public static class CycleItem {
public CharSequence label;
public long start;
public long end;
private static final StringBuilder sBuilder = new StringBuilder(50);
private static final java.util.Formatter sFormatter = new java.util.Formatter(
sBuilder, Locale.getDefault());
CycleItem(CharSequence label) {
this.label = label;
}
public CycleItem(Context context, long start, long end) {
this.label = formatDateRangeUtc(context, start, end);
this.start = start;
this.end = end;
}
private static String formatDateRangeUtc(Context context, long start, long end) {
synchronized (sBuilder) {
sBuilder.setLength(0);
return DateUtils.formatDateRange(context, sFormatter, start, end,
DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH,
Time.TIMEZONE_UTC).toString();
}
}
@Override
public String toString() {
return label.toString();
}
}
/**
* Special-case data usage cycle that triggers dialog to change
* {@link NetworkPolicy#cycleDay}.
*/
public static class CycleChangeItem extends CycleItem {
public CycleChangeItem(Context context) {
super(context.getString(R.string.data_usage_change_cycle));
}
}
public static class CycleAdapter extends ArrayAdapter<CycleItem> {
public CycleAdapter(Context context) {
super(context, android.R.layout.simple_spinner_item);
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
}
private static class AppUsageItem implements Comparable<AppUsageItem> {
public int uid;
public long total;
/** {@inheritDoc} */
public int compareTo(AppUsageItem another) {
return Long.compare(another.total, total);
}
}
/**
* Adapter of applications, sorted by total usage descending.
*/
public static class DataUsageAdapter extends BaseAdapter {
private ArrayList<AppUsageItem> mItems = Lists.newArrayList();
public void bindStats(NetworkStats stats) {
mItems.clear();
for (int i = 0; i < stats.size; i++) {
final AppUsageItem item = new AppUsageItem();
item.uid = stats.uid[i];
item.total = stats.rx[i] + stats.tx[i];
mItems.add(item);
}
Collections.sort(mItems);
notifyDataSetChanged();
}
@Override
public int getCount() {
return mItems.size();
}
@Override
public Object getItem(int position) {
return mItems.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext()).inflate(
android.R.layout.simple_list_item_2, parent, false);
}
final Context context = parent.getContext();
final PackageManager pm = context.getPackageManager();
final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);
final TextView text2 = (TextView) convertView.findViewById(android.R.id.text2);
final AppUsageItem item = mItems.get(position);
text1.setText(pm.getNameForUid(item.uid));
text2.setText(Formatter.formatFileSize(context, item.total));
return convertView;
}
}
}