diff options
Diffstat (limited to 'src/com/android/settings/DataUsageSummary.java')
-rw-r--r-- | src/com/android/settings/DataUsageSummary.java | 1090 |
1 files changed, 1090 insertions, 0 deletions
diff --git a/src/com/android/settings/DataUsageSummary.java b/src/com/android/settings/DataUsageSummary.java new file mode 100644 index 0000000..ef2282a --- /dev/null +++ b/src/com/android/settings/DataUsageSummary.java @@ -0,0 +1,1090 @@ +/* + * 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.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_WIMAX; +import static android.net.NetworkPolicy.LIMIT_DISABLED; +import static android.net.NetworkPolicyManager.ACTION_DATA_USAGE_LIMIT; +import static android.net.NetworkPolicyManager.EXTRA_NETWORK_TEMPLATE; +import static android.net.NetworkPolicyManager.computeLastCycleBoundary; +import static android.net.NetworkPolicyManager.computeNextCycleBoundary; +import static android.net.NetworkTemplate.MATCH_MOBILE_3G_LOWER; +import static android.net.NetworkTemplate.MATCH_MOBILE_4G; +import static android.net.NetworkTemplate.MATCH_MOBILE_ALL; +import static android.net.NetworkTemplate.MATCH_WIFI; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.Fragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.ConnectivityManager; +import android.net.INetworkPolicyManager; +import android.net.INetworkStatsService; +import android.net.NetworkPolicy; +import android.net.NetworkPolicyManager; +import android.net.NetworkStats; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +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.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.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.NumberPicker; +import android.widget.Spinner; +import android.widget.Switch; +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.internal.telephony.Phone; +import com.android.settings.net.NetworkPolicyEditor; +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; + +/** + * Panel show data usage history across various networks, including options to + * inspect based on usage cycle and control through {@link NetworkPolicy}. + */ +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 String TAG_CONFIRM_LIMIT = "confirmLimit"; + private static final String TAG_CYCLE_EDITOR = "cycleEditor"; + private static final String TAG_POLICY_LIMIT = "policyLimit"; + + 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 ConnectivityManager mConnService; + + private static final String PREF_FILE = "data_usage"; + private static final String PREF_SHOW_WIFI = "show_wifi"; + + private SharedPreferences mPrefs; + + private TabHost mTabHost; + private TabWidget mTabWidget; + private ListView mListView; + private DataUsageAdapter mAdapter; + + private View mHeader; + private LinearLayout mSwitches; + + private Switch mDataEnabled; + private CheckBox mDisableAtLimit; + private View mDataEnabledView; + private View mDisableAtLimitView; + + private DataUsageChartView mChart; + + private Spinner mCycleSpinner; + private CycleAdapter mCycleAdapter; + + private boolean mShowWifi = false; + + private NetworkTemplate mTemplate = null; + + private NetworkPolicyEditor mPolicyEditor; + private NetworkStatsHistory mHistory; + + private String mIntentTab = null; + + /** Flag used to ignore listeners during binding. */ + private boolean mBinding; + + @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)); + mConnService = (ConnectivityManager) getActivity().getSystemService( + Context.CONNECTIVITY_SERVICE); + mPrefs = getActivity().getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE); + + mPolicyEditor = new NetworkPolicyEditor(mPolicyService); + mPolicyEditor.read(); + + mShowWifi = mPrefs.getBoolean(PREF_SHOW_WIFI, false); + + 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 Switch(inflater.getContext()); + mDataEnabledView = inflatePreference(inflater, mSwitches, mDataEnabled); + mDataEnabled.setOnCheckedChangeListener(mDataEnabledListener); + + mDisableAtLimit = new CheckBox(inflater.getContext()); + mDisableAtLimitView = inflatePreference(inflater, mSwitches, mDisableAtLimit); + 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); + + final int chartHeight = getResources().getDimensionPixelSize( + R.dimen.data_usage_chart_height); + mChart = new DataUsageChartView(context); + mChart.setListener(mChartListener); + mChart.setLayoutParams(new AbsListView.LayoutParams(MATCH_PARENT, chartHeight)); + mListView.addHeaderView(mChart, null, false); + + mAdapter = new DataUsageAdapter(); + mListView.setOnItemClickListener(mListListener); + mListView.setAdapter(mAdapter); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + // pick default tab based on incoming intent + final Intent intent = getActivity().getIntent(); + mIntentTab = computeTabFromIntent(intent); + + // this kicks off chain reaction which creates tabs, binds the body to + // selected network, and binds chart, cycles and detail list. + updateTabs(); + + // template and tab has been selected; show dialog if limit passed + final String action = intent.getAction(); + if (ACTION_DATA_USAGE_LIMIT.equals(action)) { + PolicyLimitFragment.show(this); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.data_usage, menu); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + final Context context = getActivity(); + + final MenuItem split4g = menu.findItem(R.id.action_split_4g); + split4g.setVisible(hasMobile4gRadio(context)); + split4g.setChecked(isMobilePolicySplit()); + + final MenuItem showWifi = menu.findItem(R.id.action_show_wifi); + showWifi.setVisible(hasMobileRadio(context) && hasWifiRadio(context)); + showWifi.setChecked(mShowWifi); + + final MenuItem settings = menu.findItem(R.id.action_settings); + settings.setVisible(split4g.isVisible() || showWifi.isVisible()); + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_split_4g: { + final boolean mobileSplit = !item.isChecked(); + setMobilePolicySplit(mobileSplit); + item.setChecked(isMobilePolicySplit()); + updateTabs(); + return true; + } + case R.id.action_show_wifi: { + mShowWifi = !item.isChecked(); + mPrefs.edit().putBoolean(PREF_SHOW_WIFI, mShowWifi).apply(); + item.setChecked(mShowWifi); + updateTabs(); + return true; + } + } + return false; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + mDataEnabledView = null; + mDisableAtLimitView = null; + } + + /** + * Rebuild all tabs based on {@link NetworkPolicyEditor} 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 Context context = getActivity(); + mTabHost.clearAllTabs(); + + final boolean mobileSplit = isMobilePolicySplit(); + if (mobileSplit && hasMobile4gRadio(context)) { + mTabHost.addTab(buildTabSpec(TAB_3G, R.string.data_usage_tab_3g)); + mTabHost.addTab(buildTabSpec(TAB_4G, R.string.data_usage_tab_4g)); + } + + if (mShowWifi && hasWifiRadio(context) && hasMobileRadio(context)) { + if (!mobileSplit) { + mTabHost.addTab(buildTabSpec(TAB_MOBILE, R.string.data_usage_tab_mobile)); + } + mTabHost.addTab(buildTabSpec(TAB_WIFI, R.string.data_usage_tab_wifi)); + } + + final boolean hasTabs = mTabWidget.getTabCount() > 0; + mTabWidget.setVisibility(hasTabs ? View.VISIBLE : View.GONE); + if (hasTabs) { + if (mIntentTab != null) { + // select default tab, which will kick off updateBody() + mTabHost.setCurrentTabByTag(mIntentTab); + } else { + // select first tab, which will kick off updateBody() + mTabHost.setCurrentTab(0); + } + } else { + // no tabs visible; 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() { + mBinding = true; + + final Context context = getActivity(); + final String tabTag = mTabHost.getCurrentTabTag(); + + final String currentTab; + if (tabTag != null) { + currentTab = tabTag; + } else if (hasMobileRadio(context)) { + currentTab = TAB_MOBILE; + } else if (hasWifiRadio(context)) { + currentTab = TAB_WIFI; + } else { + throw new IllegalStateException("no mobile or wifi radios"); + } + + 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 = new NetworkTemplate(MATCH_WIFI, null); + + } else { + // make sure we show for non-wifi + mDataEnabledView.setVisibility(View.VISIBLE); + mDisableAtLimitView.setVisibility(View.VISIBLE); + } + + final String subscriberId = getActiveSubscriberId(context); + if (TAB_MOBILE.equals(currentTab)) { + setPreferenceTitle(mDataEnabledView, R.string.data_usage_enable_mobile); + setPreferenceTitle(mDisableAtLimitView, R.string.data_usage_disable_mobile_limit); + mDataEnabled.setChecked(mConnService.getMobileDataEnabled()); + mTemplate = new NetworkTemplate(MATCH_MOBILE_ALL, subscriberId); + + } else if (TAB_3G.equals(currentTab)) { + setPreferenceTitle(mDataEnabledView, R.string.data_usage_enable_3g); + setPreferenceTitle(mDisableAtLimitView, R.string.data_usage_disable_3g_limit); + // TODO: bind mDataEnabled to 3G radio state + mTemplate = new NetworkTemplate(MATCH_MOBILE_3G_LOWER, subscriberId); + + } else if (TAB_4G.equals(currentTab)) { + setPreferenceTitle(mDataEnabledView, R.string.data_usage_enable_4g); + setPreferenceTitle(mDisableAtLimitView, R.string.data_usage_disable_4g_limit); + // TODO: bind mDataEnabled to 4G radio state + mTemplate = new NetworkTemplate(MATCH_MOBILE_4G, subscriberId); + } + + 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); + + mBinding = false; + } + + private void setPolicyCycleDay(int cycleDay) { + if (LOGD) Log.d(TAG, "setPolicyCycleDay()"); + mPolicyEditor.setPolicyCycleDay(mTemplate, cycleDay); + updatePolicy(true); + } + + private void setPolicyWarningBytes(long warningBytes) { + if (LOGD) Log.d(TAG, "setPolicyWarningBytes()"); + mPolicyEditor.setPolicyWarningBytes(mTemplate, warningBytes); + updatePolicy(false); + } + + private void setPolicyLimitBytes(long limitBytes) { + if (LOGD) Log.d(TAG, "setPolicyLimitBytes()"); + mPolicyEditor.setPolicyLimitBytes(mTemplate, limitBytes); + updatePolicy(false); + } + + /** + * Update chart sweeps and cycle list to reflect {@link NetworkPolicy} for + * current {@link #mTemplate}. + */ + private void updatePolicy(boolean refreshCycle) { + final NetworkPolicy policy = mPolicyEditor.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); + } + } + + /** + * 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); + } + + private OnCheckedChangeListener mDataEnabledListener = new OnCheckedChangeListener() { + /** {@inheritDoc} */ + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (mBinding) return; + + final boolean dataEnabled = isChecked; + mDataEnabled.setChecked(dataEnabled); + + switch (mTemplate.getMatchRule()) { + case MATCH_MOBILE_ALL: { + mConnService.setMobileDataEnabled(dataEnabled); + } + } + } + }; + + private View.OnClickListener mDisableAtLimitListener = new View.OnClickListener() { + /** {@inheritDoc} */ + public void onClick(View v) { + final boolean disableAtLimit = !mDisableAtLimit.isChecked(); + if (disableAtLimit) { + // enabling limit; show confirmation dialog which eventually + // calls setPolicyLimitBytes() once user confirms. + ConfirmLimitFragment.show(DataUsageSummary.this); + } else { + setPolicyLimitBytes(LIMIT_DISABLED); + } + } + }; + + 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.putParcelable(DataUsageAppDetail.EXTRA_NETWORK_TEMPLATE, mTemplate); + args.putInt(DataUsageAppDetail.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) { + // show cycle editor; will eventually call setPolicyCycleDay() + // when user finishes editing. + CycleEditorFragment.show(DataUsageSummary.this); + + // reset spinner to something other than "change cycle..." + mCycleSpinner.setSelection(0); + + } else { + if (LOGD) { + Log.d(TAG, "showing cycle " + cycle + ", start=" + cycle.start + ", end=" + + cycle.end + "]"); + } + + // 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()"); + + new AsyncTask<Void, Void, NetworkStats>() { + @Override + protected NetworkStats doInBackground(Void... params) { + try { + final long[] range = mChart.getInspectRange(); + return mStatsService.getSummaryForAllUid(mTemplate, range[0], range[1], false); + } catch (RemoteException e) { + Log.w(TAG, "problem reading stats"); + } + return null; + } + + @Override + protected void onPostExecute(NetworkStats stats) { + if (stats != null) { + mAdapter.bindStats(stats); + } + } + }.execute(); + } + + private boolean isMobilePolicySplit() { + final String subscriberId = getActiveSubscriberId(getActivity()); + return mPolicyEditor.isMobilePolicySplit(subscriberId); + } + + private void setMobilePolicySplit(boolean split) { + final String subscriberId = getActiveSubscriberId(getActivity()); + mPolicyEditor.setMobilePolicySplit(subscriberId, split); + } + + 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() { + setPolicyWarningBytes(mChart.getWarningBytes()); + } + + /** {@inheritDoc} */ + public void onLimitChanged() { + setPolicyLimitBytes(mChart.getLimitBytes()); + } + }; + + + /** + * 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 long total = stats.rx[i] + stats.tx[i]; + final AppUsageItem item = new AppUsageItem(); + item.uid = stats.uid[i]; + item.total = total; + 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(resolveLabelForUid(pm, item.uid)); + text2.setText(Formatter.formatFileSize(context, item.total)); + + return convertView; + } + + } + + /** + * Dialog to request user confirmation before setting + * {@link NetworkPolicy#limitBytes}. + */ + public static class ConfirmLimitFragment extends DialogFragment { + public static final String EXTRA_MESSAGE_ID = "messageId"; + public static final String EXTRA_LIMIT_BYTES = "limitBytes"; + + public static void show(DataUsageSummary parent) { + final Bundle args = new Bundle(); + + // TODO: customize default limits based on network template + switch (parent.mTemplate.getMatchRule()) { + case MATCH_MOBILE_3G_LOWER: { + args.putInt(EXTRA_MESSAGE_ID, R.string.data_usage_limit_dialog_3g); + args.putLong(EXTRA_LIMIT_BYTES, 5 * GB_IN_BYTES); + break; + } + case MATCH_MOBILE_4G: { + args.putInt(EXTRA_MESSAGE_ID, R.string.data_usage_limit_dialog_4g); + args.putLong(EXTRA_LIMIT_BYTES, 5 * GB_IN_BYTES); + break; + } + case MATCH_MOBILE_ALL: { + args.putInt(EXTRA_MESSAGE_ID, R.string.data_usage_limit_dialog_mobile); + args.putLong(EXTRA_LIMIT_BYTES, 5 * GB_IN_BYTES); + break; + } + } + + final ConfirmLimitFragment dialog = new ConfirmLimitFragment(); + dialog.setArguments(args); + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getFragmentManager(), TAG_CONFIRM_LIMIT); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + + final int messageId = getArguments().getInt(EXTRA_MESSAGE_ID); + final long limitBytes = getArguments().getLong(EXTRA_LIMIT_BYTES); + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.data_usage_limit_dialog_title); + builder.setMessage(messageId); + + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + final DataUsageSummary target = (DataUsageSummary) getTargetFragment(); + if (target != null) { + target.setPolicyLimitBytes(limitBytes); + } + } + }); + + return builder.create(); + } + } + + /** + * Dialog to edit {@link NetworkPolicy#cycleDay}. + */ + public static class CycleEditorFragment extends DialogFragment { + public static final String EXTRA_CYCLE_DAY = "cycleDay"; + + public static void show(DataUsageSummary parent) { + final NetworkPolicy policy = parent.mPolicyEditor.getPolicy(parent.mTemplate); + final Bundle args = new Bundle(); + args.putInt(CycleEditorFragment.EXTRA_CYCLE_DAY, policy.cycleDay); + + final CycleEditorFragment dialog = new CycleEditorFragment(); + dialog.setArguments(args); + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getFragmentManager(), TAG_CYCLE_EDITOR); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); + + final View view = dialogInflater.inflate(R.layout.data_usage_cycle_editor, null, false); + final NumberPicker cycleDayPicker = (NumberPicker) view.findViewById(R.id.cycle_day); + + final int oldCycleDay = getArguments().getInt(EXTRA_CYCLE_DAY, 1); + + cycleDayPicker.setMinValue(1); + cycleDayPicker.setMaxValue(31); + cycleDayPicker.setValue(oldCycleDay); + cycleDayPicker.setWrapSelectorWheel(true); + + builder.setTitle(R.string.data_usage_cycle_editor_title); + builder.setView(view); + + builder.setPositiveButton(R.string.data_usage_cycle_editor_positive, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + final int cycleDay = cycleDayPicker.getValue(); + final DataUsageSummary target = (DataUsageSummary) getTargetFragment(); + if (target != null) { + target.setPolicyCycleDay(cycleDay); + } + } + }); + + return builder.create(); + } + } + + /** + * Dialog explaining that {@link NetworkPolicy#limitBytes} has been passed, + * and giving the user an option to bypass. + */ + public static class PolicyLimitFragment extends DialogFragment { + public static final String EXTRA_TITLE_ID = "titleId"; + + public static void show(DataUsageSummary parent) { + final Bundle args = new Bundle(); + + switch (parent.mTemplate.getMatchRule()) { + case MATCH_MOBILE_3G_LOWER: { + args.putInt(EXTRA_TITLE_ID, R.string.data_usage_disabled_dialog_3g_title); + break; + } + case MATCH_MOBILE_4G: { + args.putInt(EXTRA_TITLE_ID, R.string.data_usage_disabled_dialog_4g_title); + break; + } + case MATCH_MOBILE_ALL: { + args.putInt(EXTRA_TITLE_ID, R.string.data_usage_disabled_dialog_mobile_title); + break; + } + } + + final PolicyLimitFragment dialog = new PolicyLimitFragment(); + dialog.setArguments(args); + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getFragmentManager(), TAG_POLICY_LIMIT); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + + final int titleId = getArguments().getInt(EXTRA_TITLE_ID); + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(titleId); + builder.setMessage(R.string.data_usage_disabled_dialog); + + builder.setPositiveButton(android.R.string.ok, null); + builder.setNegativeButton(R.string.data_usage_disabled_dialog_enable, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + final DataUsageSummary target = (DataUsageSummary) getTargetFragment(); + if (target != null) { + // TODO: consider "allow 100mb more data", or + // only bypass limit for current cycle. + target.setPolicyLimitBytes(LIMIT_DISABLED); + } + } + }); + + return builder.create(); + } + } + + /** + * Compute default tab that should be selected, based on + * {@link NetworkPolicyManager#EXTRA_NETWORK_TEMPLATE} extra. + */ + private static String computeTabFromIntent(Intent intent) { + final int networkTemplate = intent.getIntExtra(EXTRA_NETWORK_TEMPLATE, TEMPLATE_INVALID); + switch (networkTemplate) { + case MATCH_MOBILE_3G_LOWER: + return TAB_3G; + case MATCH_MOBILE_4G: + return TAB_4G; + case MATCH_MOBILE_ALL: + return TAB_MOBILE; + case MATCH_WIFI: + return TAB_WIFI; + default: + return null; + } + } + + /** + * Resolve best descriptive label for the given UID. + */ + public static CharSequence resolveLabelForUid(PackageManager pm, int uid) { + final String[] packageNames = pm.getPackagesForUid(uid); + final int length = packageNames != null ? packageNames.length : 0; + + CharSequence label = pm.getNameForUid(uid); + try { + if (length == 1) { + final ApplicationInfo info = pm.getApplicationInfo(packageNames[0], 0); + label = info.loadLabel(pm); + } else if (length > 1) { + for (String packageName : packageNames) { + final PackageInfo info = pm.getPackageInfo(packageName, 0); + if (info.sharedUserLabel != 0) { + label = pm.getText(packageName, info.sharedUserLabel, info.applicationInfo); + if (!TextUtils.isEmpty(label)) { + break; + } + } + } + } + } catch (NameNotFoundException e) { + } + + if (TextUtils.isEmpty(label)) { + label = Integer.toString(uid); + } + return label; + } + + /** + * Test if device has a mobile data radio. + */ + private static boolean hasMobileRadio(Context context) { + final ConnectivityManager conn = (ConnectivityManager) context.getSystemService( + Context.CONNECTIVITY_SERVICE); + + // mobile devices should have MOBILE network tracker regardless of + // connection status. + return conn.getNetworkInfo(TYPE_MOBILE) != null; + } + + /** + * Test if device has a mobile 4G data radio. + */ + private static boolean hasMobile4gRadio(Context context) { + final ConnectivityManager conn = (ConnectivityManager) context.getSystemService( + Context.CONNECTIVITY_SERVICE); + final TelephonyManager telephony = (TelephonyManager) context.getSystemService( + Context.TELEPHONY_SERVICE); + + // WiMAX devices should have WiMAX network tracker regardless of + // connection status. + final boolean hasWimax = conn.getNetworkInfo(TYPE_WIMAX) != null; + final boolean hasLte = telephony.getLteOnCdmaMode() == Phone.LTE_ON_CDMA_TRUE; + return hasWimax || hasLte; + } + + /** + * Test if device has a Wi-Fi data radio. + */ + private static boolean hasWifiRadio(Context context) { + return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI); + } + + /** + * Inflate a {@link Preference} style layout, adding the given {@link View} + * widget into {@link android.R.id#widget_frame}. + */ + private static View inflatePreference(LayoutInflater inflater, ViewGroup root, View widget) { + final View view = inflater.inflate(R.layout.preference, root, false); + final LinearLayout widgetFrame = (LinearLayout) view.findViewById( + android.R.id.widget_frame); + widgetFrame.addView(widget, new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + return view; + } + + /** + * Set {@link android.R.id#title} for a preference view inflated with + * {@link #inflatePreference(LayoutInflater, View, View)}. + */ + private static void setPreferenceTitle(View parent, int resId) { + final TextView title = (TextView) parent.findViewById(android.R.id.title); + title.setText(resId); + } + +} |