diff --git a/src/com/android/settings/DataUsageSummary.java b/src/com/android/settings/DataUsageSummary.java index e27227f301b..44a86dfdeb0 100644 --- a/src/com/android/settings/DataUsageSummary.java +++ b/src/com/android/settings/DataUsageSummary.java @@ -16,6 +16,7 @@ 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; @@ -37,6 +38,8 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.preference.CheckBoxPreference; import android.preference.Preference; +import android.preference.SwitchPreference; +import android.telephony.TelephonyManager; import android.text.format.DateUtils; import android.text.format.Formatter; import android.text.format.Time; @@ -64,6 +67,7 @@ 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; @@ -99,7 +103,7 @@ public class DataUsageSummary extends Fragment { private View mHeader; private LinearLayout mSwitches; - private CheckBoxPreference mDataEnabled; + private SwitchPreference mDataEnabled; private CheckBoxPreference mDisableAtLimit; private View mDataEnabledView; private View mDisableAtLimitView; @@ -109,25 +113,28 @@ public class DataUsageSummary extends Fragment { private Spinner mCycleSpinner; private CycleAdapter mCycleAdapter; - private boolean mSplit4G = false; + // TODO: persist show wifi flag private boolean mShowWifi = false; private int mTemplate = TEMPLATE_INVALID; - private NetworkPolicy mPolicy; + private NetworkPolicyModifier mPolicyModifier; private NetworkStatsHistory mHistory; - // TODO: policy service should always provide valid stub policy - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setHasOptionsMenu(true); 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); + + setHasOptionsMenu(true); } @Override @@ -147,7 +154,7 @@ public class DataUsageSummary extends Fragment { mHeader = inflater.inflate(R.layout.data_usage_header, mListView, false); mListView.addHeaderView(mHeader, null, false); - mDataEnabled = new CheckBoxPreference(context); + mDataEnabled = new SwitchPreference(context); mDisableAtLimit = new CheckBoxPreference(context); // kick refresh once to force-create views @@ -185,6 +192,9 @@ public class DataUsageSummary extends Fragment { public void onResume() { super.onResume(); + // read current policy state from service + mPolicyModifier.read(); + // this kicks off chain reaction which creates tabs, binds the body to // selected network, and binds chart, cycles and detail list. updateTabs(); @@ -196,13 +206,18 @@ public class DataUsageSummary extends Fragment { } @Override - public boolean onOptionsItemSelected(MenuItem item) { - // TODO: persist checked-ness of options to restore tabs later + 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: { - mSplit4G = !item.isChecked(); - item.setChecked(mSplit4G); + final boolean mobileSplit = !item.isChecked(); + mPolicyModifier.setMobilePolicySplit(mobileSplit); + item.setChecked(mPolicyModifier.isMobilePolicySplit()); updateTabs(); return true; } @@ -217,24 +232,23 @@ public class DataUsageSummary extends Fragment { } /** - * Rebuild all tabs based on {@link #mSplit4G} and {@link #mShowWifi}, - * hiding the tabs entirely when applicable. Selects first tab, and kicks - * off a full rebind of body contents. + * 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() { - // TODO: persist/restore if user wants mobile split, or wifi visibility - - final boolean tabsVisible = mSplit4G || mShowWifi; + final boolean mobileSplit = mPolicyModifier.isMobilePolicySplit(); + final boolean tabsVisible = mobileSplit || mShowWifi; mTabWidget.setVisibility(tabsVisible ? View.VISIBLE : View.GONE); mTabHost.clearAllTabs(); - if (mSplit4G) { + 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 (!mSplit4G) { + if (!mobileSplit) { mTabHost.addTab(buildTabSpec(TAB_MOBILE, R.string.data_usage_tab_mobile)); } mTabHost.addTab(buildTabSpec(TAB_WIFI, R.string.data_usage_tab_wifi)); @@ -323,8 +337,7 @@ public class DataUsageSummary extends Fragment { mDataEnabled.setChecked(true); try { - // load policy and stats for current template - mPolicy = mPolicyService.getNetworkPolicy(mTemplate, null); + // 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 @@ -332,20 +345,10 @@ public class DataUsageSummary extends Fragment { throw new RuntimeException("problem reading network policy or stats", e); } - // TODO: eventually service will always provide stub policy - if (mPolicy == null) { - mPolicy = new NetworkPolicy(1, 4 * GB_IN_BYTES, -1); - } - // bind chart to historical stats - mChart.bindNetworkPolicy(mPolicy); mChart.bindNetworkStats(mHistory); - // generate cycle list based on policy and available history - updateCycleList(); - - // reflect policy limit in checkbox - mDisableAtLimit.setChecked(mPolicy.limitBytes != -1); + updatePolicy(); // force scroll to top of body mListView.smoothScrollToPosition(0); @@ -354,6 +357,24 @@ public class DataUsageSummary extends Fragment { refreshPreferenceViews(); } + /** + * Update chart sweeps and cycle list to reflect {@link NetworkPolicy} for + * current {@link #mTemplate}. + */ + private void updatePolicy() { + final NetworkPolicy policy = mPolicyModifier.getPolicy(mTemplate); + + // reflect policy limit in checkbox + mDisableAtLimit.setChecked(policy != null && policy.limitBytes != LIMIT_DISABLED); + mChart.bindNetworkPolicy(policy); + + // 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}. @@ -376,7 +397,7 @@ public class DataUsageSummary extends Fragment { * and available {@link NetworkStatsHistory} data. Always selects the newest * item, updating the inspection range on {@link #mChart}. */ - private void updateCycleList() { + private void updateCycleList(NetworkPolicy policy) { mCycleAdapter.clear(); final Context context = mCycleSpinner.getContext(); @@ -385,28 +406,36 @@ public class DataUsageSummary extends Fragment { final long historyStart = bounds[0]; final long historyEnd = bounds[1]; - // find the next cycle boundary - long cycleEnd = computeNextCycleBoundary(historyEnd, mPolicy); + if (policy != null) { + // find the next cycle boundary + long cycleEnd = computeNextCycleBoundary(historyEnd, policy); - int guardCount = 0; + int guardCount = 0; - // walk backwards, generating all valid cycle ranges - while (cycleEnd > historyStart) { - final long cycleStart = computeLastCycleBoundary(cycleEnd, mPolicy); - Log.d(TAG, "generating cs=" + cycleStart + " to ce=" + cycleEnd + " waiting for hs=" - + historyStart); - mCycleAdapter.add(new CycleItem(context, cycleStart, cycleEnd)); - cycleEnd = cycleStart; + // 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=" + mPolicy); + // 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 change date - mCycleAdapter.add(new CycleChangeItem(context)); + // 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); @@ -438,11 +467,11 @@ public class DataUsageSummary extends Fragment { mDisableAtLimit.setChecked(disableAtLimit); refreshPreferenceViews(); - // TODO: push updated policy to service + // TODO: create policy if none exists // TODO: show interstitial warning dialog to user - final long limitBytes = disableAtLimit ? 5 * GB_IN_BYTES : -1; - mPolicy = new NetworkPolicy(mPolicy.cycleDay, mPolicy.warningBytes, limitBytes); - mChart.bindNetworkPolicy(mPolicy); + final long limitBytes = disableAtLimit ? 5 * GB_IN_BYTES : LIMIT_DISABLED; + mPolicyModifier.setPolicyLimitBytes(mTemplate, limitBytes); + updatePolicy(); } }; @@ -500,6 +529,12 @@ public class DataUsageSummary extends Fragment { } } + 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() { @@ -508,26 +543,20 @@ public class DataUsageSummary extends Fragment { } /** {@inheritDoc} */ - public void onLimitsChanged() { - if (LOGD) Log.d(TAG, "onLimitsChanged()"); - - // redefine policy and persist into service - // TODO: kick this onto background thread, since service touches disk - - // TODO: remove this mPolicy null check, since later service will - // always define baseline value. - final int cycleDay = mPolicy != null ? mPolicy.cycleDay : 1; + public void onWarningChanged() { + if (LOGD) Log.d(TAG, "onWarningChanged()"); final long warningBytes = mChart.getWarningBytes(); - final long limitBytes = mDisableAtLimit.isChecked() ? -1 : mChart.getLimitBytes(); + mPolicyModifier.setPolicyWarningBytes(mTemplate, warningBytes); + updatePolicy(); + } - mPolicy = new NetworkPolicy(cycleDay, warningBytes, limitBytes); - if (LOGD) Log.d(TAG, "persisting policy=" + mPolicy); - - try { - mPolicyService.setNetworkPolicy(mTemplate, null, mPolicy); - } catch (RemoteException e) { - Log.w(TAG, "problem persisting policy", e); - } + /** {@inheritDoc} */ + public void onLimitChanged() { + if (LOGD) Log.d(TAG, "onLimitChanged()"); + final long limitBytes = mDisableAtLimit.isChecked() ? mChart.getLimitBytes() + : LIMIT_DISABLED; + mPolicyModifier.setPolicyLimitBytes(mTemplate, limitBytes); + updatePolicy(); } }; @@ -605,7 +634,7 @@ public class DataUsageSummary extends Fragment { public void bindStats(NetworkStats stats) { mItems.clear(); - for (int i = 0; i < stats.length(); i++) { + 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]; diff --git a/src/com/android/settings/net/NetworkPolicyModifier.java b/src/com/android/settings/net/NetworkPolicyModifier.java new file mode 100644 index 00000000000..1d8aca39fdf --- /dev/null +++ b/src/com/android/settings/net/NetworkPolicyModifier.java @@ -0,0 +1,145 @@ +/* + * 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.net; + +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 com.android.internal.util.Preconditions.checkNotNull; + +import android.net.INetworkPolicyManager; +import android.net.NetworkPolicy; +import android.os.AsyncTask; +import android.os.RemoteException; + +import com.android.internal.util.Objects; +import com.google.android.collect.Lists; + +import java.util.ArrayList; + +/** + * Utility class to modify list of {@link NetworkPolicy}. Specifically knows + * about which policies can coexist. + */ +public class NetworkPolicyModifier { + + private INetworkPolicyManager mPolicyService; + private String mSubscriberId; + + private ArrayList mPolicies = Lists.newArrayList(); + + public NetworkPolicyModifier(INetworkPolicyManager policyService, String subscriberId) { + mPolicyService = checkNotNull(policyService); + mSubscriberId = subscriberId; + } + + public void read() { + try { + final NetworkPolicy[] policies = mPolicyService.getNetworkPolicies(); + mPolicies.clear(); + for (NetworkPolicy policy : policies) { + mPolicies.add(policy); + } + } catch (RemoteException e) { + throw new RuntimeException("problem reading policies", e); + } + } + + public void writeAsync() { + // TODO: consider making more robust by passing through service + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + write(); + return null; + } + }.execute(); + } + + public void write() { + try { + final NetworkPolicy[] policies = mPolicies.toArray(new NetworkPolicy[mPolicies.size()]); + mPolicyService.setNetworkPolicies(policies); + } catch (RemoteException e) { + throw new RuntimeException("problem reading policies", e); + } + } + + public NetworkPolicy getPolicy(int networkTemplate) { + for (NetworkPolicy policy : mPolicies) { + if (policy.networkTemplate == networkTemplate + && Objects.equal(policy.subscriberId, mSubscriberId)) { + return policy; + } + } + return null; + } + + public void setPolicyCycleDay(int networkTemplate, int cycleDay) { + getPolicy(networkTemplate).cycleDay = cycleDay; + writeAsync(); + } + + public void setPolicyWarningBytes(int networkTemplate, long warningBytes) { + getPolicy(networkTemplate).warningBytes = warningBytes; + writeAsync(); + } + + public void setPolicyLimitBytes(int networkTemplate, long limitBytes) { + getPolicy(networkTemplate).limitBytes = limitBytes; + writeAsync(); + } + + public boolean isMobilePolicySplit() { + return getPolicy(TEMPLATE_MOBILE_3G_LOWER) != null && getPolicy(TEMPLATE_MOBILE_4G) != null; + } + + public void setMobilePolicySplit(boolean split) { + final boolean beforeSplit = isMobilePolicySplit(); + if (split == beforeSplit) { + // already in requested state; skip + return; + + } else if (beforeSplit && !split) { + // combine, picking most restrictive policy + final NetworkPolicy policy3g = getPolicy(TEMPLATE_MOBILE_3G_LOWER); + final NetworkPolicy policy4g = getPolicy(TEMPLATE_MOBILE_4G); + + final NetworkPolicy restrictive = policy3g.compareTo(policy4g) < 0 ? policy3g + : policy4g; + mPolicies.remove(policy3g); + mPolicies.remove(policy4g); + mPolicies.add(new NetworkPolicy(TEMPLATE_MOBILE_ALL, restrictive.subscriberId, + restrictive.cycleDay, restrictive.warningBytes, restrictive.limitBytes)); + writeAsync(); + + } else if (!beforeSplit && split) { + // duplicate existing policy into two rules + final NetworkPolicy policyAll = getPolicy(TEMPLATE_MOBILE_ALL); + mPolicies.remove(policyAll); + mPolicies.add( + new NetworkPolicy(TEMPLATE_MOBILE_3G_LOWER, policyAll.subscriberId, + policyAll.cycleDay, policyAll.warningBytes, policyAll.limitBytes)); + mPolicies.add( + new NetworkPolicy(TEMPLATE_MOBILE_4G, policyAll.subscriberId, + policyAll.cycleDay, policyAll.warningBytes, policyAll.limitBytes)); + writeAsync(); + + } + } + +} diff --git a/src/com/android/settings/widget/DataUsageChartView.java b/src/com/android/settings/widget/DataUsageChartView.java index defa9533c0a..6a702d0ef74 100644 --- a/src/com/android/settings/widget/DataUsageChartView.java +++ b/src/com/android/settings/widget/DataUsageChartView.java @@ -21,6 +21,7 @@ import android.graphics.Color; import android.net.NetworkPolicy; import android.net.NetworkStatsHistory; import android.text.format.DateUtils; +import android.view.View; import com.android.settings.widget.ChartSweepView.OnSweepListener; @@ -44,7 +45,8 @@ public class DataUsageChartView extends ChartView { public interface DataUsageChartListener { public void onInspectRangeChanged(); - public void onLimitsChanged(); + public void onWarningChanged(); + public void onLimitChanged(); } private DataUsageChartListener mListener; @@ -78,6 +80,9 @@ public class DataUsageChartView extends ChartView { mSeries.bindSweepRange(mSweepTime1, mSweepTime2); + mSweepDataWarn.addOnSweepListener(mWarningListener); + mSweepDataLimit.addOnSweepListener(mLimitListener); + mSweepTime1.addOnSweepListener(mSweepListener); mSweepTime2.addOnSweepListener(mSweepListener); @@ -92,15 +97,29 @@ public class DataUsageChartView extends ChartView { } public void bindNetworkPolicy(NetworkPolicy policy) { - if (policy.limitBytes != -1) { + if (policy == null) { + mSweepDataLimit.setVisibility(View.INVISIBLE); + mSweepDataWarn.setVisibility(View.INVISIBLE); + return; + } + + if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) { + mSweepDataLimit.setVisibility(View.VISIBLE); mSweepDataLimit.setValue(policy.limitBytes); mSweepDataLimit.setEnabled(true); } else { + // TODO: set limit default based on axis maximum + mSweepDataLimit.setVisibility(View.VISIBLE); mSweepDataLimit.setValue(5 * GB_IN_BYTES); mSweepDataLimit.setEnabled(false); } - mSweepDataWarn.setValue(policy.warningBytes); + if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) { + mSweepDataWarn.setVisibility(View.VISIBLE); + mSweepDataWarn.setValue(policy.warningBytes); + } else { + mSweepDataWarn.setVisibility(View.INVISIBLE); + } } private OnSweepListener mSweepListener = new OnSweepListener() { @@ -115,6 +134,22 @@ public class DataUsageChartView extends ChartView { } }; + private OnSweepListener mWarningListener = new OnSweepListener() { + public void onSweep(ChartSweepView sweep, boolean sweepDone) { + if (sweepDone && mListener != null) { + mListener.onWarningChanged(); + } + } + }; + + private OnSweepListener mLimitListener = new OnSweepListener() { + public void onSweep(ChartSweepView sweep, boolean sweepDone) { + if (sweepDone && mListener != null) { + mListener.onLimitChanged(); + } + } + }; + /** * Return current inspection range (start and end time) based on internal * {@link ChartSweepView} positions.