First pass at detailed app data usage, policy.

Fragment to show application data usage details, including chart with
inspection ranges.  Button that invokes ACTION_MANAGE_NETWORK_USAGE
towards application, and UID-specific policy controls.  Fragment is
launched when clicking list items from data usage summary page.

Change-Id: Ie1564aa8af98e1a7083817a997059a5a7b1caa50
This commit is contained in:
Jeff Sharkey
2011-06-13 00:42:03 -07:00
parent 05cc0cc4a6
commit 4dfa66001d
6 changed files with 337 additions and 30 deletions

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/chart_container"
android:layout_width="match_parent"
android:layout_height="233dip" />
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dip" />
<TextView
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dip" />
<Button
android:id="@+id/data_usage_app_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dip"
android:text="@string/data_usage_app_settings" />
<LinearLayout
android:id="@+id/switches"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</ScrollView>

View File

@@ -3395,4 +3395,11 @@ found in the list of installed applications.</string>
<!-- Toggle switch title for enabling 4G data network connection. [CHAR LIMIT=32] --> <!-- Toggle switch title for enabling 4G data network connection. [CHAR LIMIT=32] -->
<string name="data_usage_enable_4g">4G data</string> <string name="data_usage_enable_4g">4G data</string>
<!-- Button title for launching application-specific data usage settings. [CHAR LIMIT=32] -->
<string name="data_usage_app_settings">View application settings</string>
<!-- Checkbox label that restricts background data usage of a specific application. [CHAR LIMIT=32] -->
<string name="data_usage_app_restrict_background">Restrict background data usage</string>
<!-- Summary message for checkbox that restricts background data usage of a specific application. [CHAR LIMIT=32] -->
<string name="data_usage_app_restrict_background_summary">Only allow application background data when using an unlimited network</string>
</resources> </resources>

View File

@@ -0,0 +1,224 @@
/*
* 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.NetworkPolicyManager.POLICY_NONE;
import static android.net.NetworkPolicyManager.POLICY_REJECT_PAID_BACKGROUND;
import static android.net.TrafficStats.TEMPLATE_MOBILE_ALL;
import static com.android.settings.DataUsageSummary.getHistoryBounds;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.INetworkPolicyManager;
import android.net.INetworkStatsService;
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.text.format.DateUtils;
import android.text.format.Formatter;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.settings.widget.DataUsageChartView;
import com.android.settings.widget.DataUsageChartView.DataUsageChartListener;
public class DataUsageAppDetail extends Fragment {
private static final String TAG = "DataUsage";
private static final boolean LOGD = true;
private int mUid;
private INetworkStatsService mStatsService;
private INetworkPolicyManager mPolicyService;
private CheckBoxPreference mRestrictBackground;
private View mRestrictBackgroundView;
private FrameLayout mChartContainer;
private TextView mTitle;
private TextView mText1;
private Button mAppSettings;
private LinearLayout mSwitches;
private DataUsageChartView mChart;
private int mUidPolicy;
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));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final Context context = inflater.getContext();
final View view = inflater.inflate(R.layout.data_usage_detail, container, false);
mChartContainer = (FrameLayout) view.findViewById(R.id.chart_container);
mTitle = (TextView) view.findViewById(android.R.id.title);
mText1 = (TextView) view.findViewById(android.R.id.text1);
mAppSettings = (Button) view.findViewById(R.id.data_usage_app_settings);
mSwitches = (LinearLayout) view.findViewById(R.id.switches);
mRestrictBackground = new CheckBoxPreference(context);
mRestrictBackground.setTitle(R.string.data_usage_app_restrict_background);
mRestrictBackground.setSummary(R.string.data_usage_app_restrict_background_summary);
// kick refresh once to force-create views
refreshPreferenceViews();
mSwitches.addView(mRestrictBackgroundView);
mRestrictBackgroundView.setOnClickListener(mRestrictBackgroundListener);
mAppSettings.setOnClickListener(mAppSettingsListener);
mChart = new DataUsageChartView(context);
mChartContainer.addView(mChart);
mChart.setListener(mChartListener);
mChart.setChartColor(Color.parseColor("#d88d3a"), Color.parseColor("#c0ba7f3e"),
Color.parseColor("#88566abc"));
return view;
}
@Override
public void onResume() {
super.onResume();
final Context context = getActivity();
mUid = getArguments().getInt(Intent.EXTRA_UID);
mTitle.setText(context.getPackageManager().getNameForUid(mUid));
updateBody();
}
private void updateBody() {
try {
// load stats for current uid and template
// TODO: read template from extras
mUidPolicy = mPolicyService.getUidPolicy(mUid);
mHistory = mStatsService.getHistoryForUid(mUid, TEMPLATE_MOBILE_ALL);
} 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 stats", e);
}
// bind chart to historical stats
mChart.bindNetworkStats(mHistory);
// show entire history known
final long[] bounds = getHistoryBounds(mHistory);
mChart.setVisibleRange(bounds[0], bounds[1] + DateUtils.WEEK_IN_MILLIS, bounds[1]);
updateDetailData();
// update policy checkbox
final boolean restrictBackground = (mUidPolicy & POLICY_REJECT_PAID_BACKGROUND) != 0;
mRestrictBackground.setChecked(restrictBackground);
// kick preference views so they rebind from changes above
refreshPreferenceViews();
}
private void updateDetailData() {
if (LOGD) Log.d(TAG, "updateDetailData()");
final Context context = mChart.getContext();
final long[] range = mChart.getInspectRange();
final long[] total = mHistory.getTotalData(range[0], range[1], null);
final long totalCombined = total[0] + total[1];
mText1.setText(Formatter.formatFileSize(context, totalCombined));
}
/**
* Force rebind of hijacked {@link Preference} views.
*/
private void refreshPreferenceViews() {
mRestrictBackgroundView = mRestrictBackground.getView(mRestrictBackgroundView, mSwitches);
}
private DataUsageChartListener mChartListener = new DataUsageChartListener() {
/** {@inheritDoc} */
public void onInspectRangeChanged() {
if (LOGD) Log.d(TAG, "onInspectRangeChanged()");
updateDetailData();
}
/** {@inheritDoc} */
public void onWarningChanged() {
// ignored
}
/** {@inheritDoc} */
public void onLimitChanged() {
// ignored
}
};
private OnClickListener mAppSettingsListener = new OnClickListener() {
/** {@inheritDoc} */
public void onClick(View v) {
// TODO: target torwards entire UID instead of just first package
final PackageManager pm = getActivity().getPackageManager();
final String packageName = pm.getPackagesForUid(mUid)[0];
final Intent intent = new Intent(Intent.ACTION_MANAGE_NETWORK_USAGE);
intent.setPackage(packageName);
startActivity(intent);
}
};
private OnClickListener mRestrictBackgroundListener = new OnClickListener() {
/** {@inheritDoc} */
public void onClick(View v) {
final boolean restrictBackground = !mRestrictBackground.isChecked();
mRestrictBackground.setChecked(restrictBackground);
refreshPreferenceViews();
try {
mPolicyService.setUidPolicy(
mUid, restrictBackground ? POLICY_REJECT_PAID_BACKGROUND : POLICY_NONE);
} catch (RemoteException e) {
throw new RuntimeException("unable to save policy", e);
}
}
};
}

View File

@@ -27,6 +27,7 @@ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import android.app.Fragment; import android.app.Fragment;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.INetworkPolicyManager; import android.net.INetworkPolicyManager;
import android.net.INetworkStatsService; import android.net.INetworkStatsService;
@@ -38,6 +39,7 @@ import android.os.RemoteException;
import android.os.ServiceManager; import android.os.ServiceManager;
import android.preference.CheckBoxPreference; import android.preference.CheckBoxPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.SwitchPreference; import android.preference.SwitchPreference;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.text.format.DateUtils; import android.text.format.DateUtils;
@@ -348,7 +350,7 @@ public class DataUsageSummary extends Fragment {
// bind chart to historical stats // bind chart to historical stats
mChart.bindNetworkStats(mHistory); mChart.bindNetworkStats(mHistory);
updatePolicy(); updatePolicy(true);
// force scroll to top of body // force scroll to top of body
mListView.smoothScrollToPosition(0); mListView.smoothScrollToPosition(0);
@@ -361,15 +363,17 @@ public class DataUsageSummary extends Fragment {
* Update chart sweeps and cycle list to reflect {@link NetworkPolicy} for * Update chart sweeps and cycle list to reflect {@link NetworkPolicy} for
* current {@link #mTemplate}. * current {@link #mTemplate}.
*/ */
private void updatePolicy() { private void updatePolicy(boolean refreshCycle) {
final NetworkPolicy policy = mPolicyModifier.getPolicy(mTemplate); final NetworkPolicy policy = mPolicyModifier.getPolicy(mTemplate);
// reflect policy limit in checkbox // reflect policy limit in checkbox
mDisableAtLimit.setChecked(policy != null && policy.limitBytes != LIMIT_DISABLED); mDisableAtLimit.setChecked(policy != null && policy.limitBytes != LIMIT_DISABLED);
mChart.bindNetworkPolicy(policy); mChart.bindNetworkPolicy(policy);
// generate cycle list based on policy and available history if (refreshCycle) {
updateCycleList(policy); // generate cycle list based on policy and available history
updateCycleList(policy);
}
// kick preference views so they rebind from changes above // kick preference views so they rebind from changes above
refreshPreferenceViews(); refreshPreferenceViews();
@@ -379,7 +383,7 @@ public class DataUsageSummary extends Fragment {
* Return full time bounds (earliest and latest time recorded) of the given * Return full time bounds (earliest and latest time recorded) of the given
* {@link NetworkStatsHistory}. * {@link NetworkStatsHistory}.
*/ */
private static long[] getHistoryBounds(NetworkStatsHistory history) { public static long[] getHistoryBounds(NetworkStatsHistory history) {
final long currentTime = System.currentTimeMillis(); final long currentTime = System.currentTimeMillis();
long start = currentTime; long start = currentTime;
@@ -471,17 +475,21 @@ public class DataUsageSummary extends Fragment {
// TODO: show interstitial warning dialog to user // TODO: show interstitial warning dialog to user
final long limitBytes = disableAtLimit ? 5 * GB_IN_BYTES : LIMIT_DISABLED; final long limitBytes = disableAtLimit ? 5 * GB_IN_BYTES : LIMIT_DISABLED;
mPolicyModifier.setPolicyLimitBytes(mTemplate, limitBytes); mPolicyModifier.setPolicyLimitBytes(mTemplate, limitBytes);
updatePolicy(); updatePolicy(false);
} }
}; };
private OnItemClickListener mListListener = new OnItemClickListener() { private OnItemClickListener mListListener = new OnItemClickListener() {
/** {@inheritDoc} */ /** {@inheritDoc} */
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final Object object = parent.getItemAtPosition(position); final AppUsageItem app = (AppUsageItem) parent.getItemAtPosition(position);
// TODO: show app details final Bundle args = new Bundle();
Log.d(TAG, "showing app details for " + object); 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);
} }
}; };
@@ -547,7 +555,7 @@ public class DataUsageSummary extends Fragment {
if (LOGD) Log.d(TAG, "onWarningChanged()"); if (LOGD) Log.d(TAG, "onWarningChanged()");
final long warningBytes = mChart.getWarningBytes(); final long warningBytes = mChart.getWarningBytes();
mPolicyModifier.setPolicyWarningBytes(mTemplate, warningBytes); mPolicyModifier.setPolicyWarningBytes(mTemplate, warningBytes);
updatePolicy(); updatePolicy(false);
} }
/** {@inheritDoc} */ /** {@inheritDoc} */
@@ -556,7 +564,7 @@ public class DataUsageSummary extends Fragment {
final long limitBytes = mDisableAtLimit.isChecked() ? mChart.getLimitBytes() final long limitBytes = mDisableAtLimit.isChecked() ? mChart.getLimitBytes()
: LIMIT_DISABLED; : LIMIT_DISABLED;
mPolicyModifier.setPolicyLimitBytes(mTemplate, limitBytes); mPolicyModifier.setPolicyLimitBytes(mTemplate, limitBytes);
updatePolicy(); updatePolicy(false);
} }
}; };
@@ -615,22 +623,22 @@ public class DataUsageSummary extends Fragment {
} }
} }
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. * Adapter of applications, sorted by total usage descending.
*/ */
public static class DataUsageAdapter extends BaseAdapter { public static class DataUsageAdapter extends BaseAdapter {
private ArrayList<AppUsageItem> mItems = Lists.newArrayList(); private ArrayList<AppUsageItem> mItems = Lists.newArrayList();
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);
}
}
public void bindStats(NetworkStats stats) { public void bindStats(NetworkStats stats) {
mItems.clear(); mItems.clear();

View File

@@ -40,9 +40,9 @@ public class ChartNetworkSeriesView extends View {
private final ChartAxis mHoriz; private final ChartAxis mHoriz;
private final ChartAxis mVert; private final ChartAxis mVert;
private final Paint mPaintStroke; private Paint mPaintStroke;
private final Paint mPaintFill; private Paint mPaintFill;
private final Paint mPaintFillDisabled; private Paint mPaintFillDisabled;
private NetworkStatsHistory mStats; private NetworkStatsHistory mStats;
@@ -58,24 +58,29 @@ public class ChartNetworkSeriesView extends View {
mHoriz = Preconditions.checkNotNull(horiz, "missing horiz"); mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
mVert = Preconditions.checkNotNull(vert, "missing vert"); mVert = Preconditions.checkNotNull(vert, "missing vert");
setChartColor(Color.parseColor("#24aae1"), Color.parseColor("#c050ade5"),
Color.parseColor("#88566abc"));
mPathStroke = new Path();
mPathFill = new Path();
}
public void setChartColor(int stroke, int fill, int disabled) {
mPaintStroke = new Paint(); mPaintStroke = new Paint();
mPaintStroke.setStrokeWidth(6.0f); mPaintStroke.setStrokeWidth(6.0f);
mPaintStroke.setColor(Color.parseColor("#24aae1")); mPaintStroke.setColor(stroke);
mPaintStroke.setStyle(Style.STROKE); mPaintStroke.setStyle(Style.STROKE);
mPaintStroke.setAntiAlias(true); mPaintStroke.setAntiAlias(true);
mPaintFill = new Paint(); mPaintFill = new Paint();
mPaintFill.setColor(Color.parseColor("#c050ade5")); mPaintFill.setColor(fill);
mPaintFill.setStyle(Style.FILL); mPaintFill.setStyle(Style.FILL);
mPaintFill.setAntiAlias(true); mPaintFill.setAntiAlias(true);
mPaintFillDisabled = new Paint(); mPaintFillDisabled = new Paint();
mPaintFillDisabled.setColor(Color.parseColor("#88566abc")); mPaintFillDisabled.setColor(disabled);
mPaintFillDisabled.setStyle(Style.FILL); mPaintFillDisabled.setStyle(Style.FILL);
mPaintFillDisabled.setAntiAlias(true); mPaintFillDisabled.setAntiAlias(true);
mPathStroke = new Path();
mPathFill = new Path();
} }
public void bindNetworkStats(NetworkStatsHistory stats) { public void bindNetworkStats(NetworkStatsHistory stats) {

View File

@@ -86,6 +86,13 @@ public class DataUsageChartView extends ChartView {
mSweepTime1.addOnSweepListener(mSweepListener); mSweepTime1.addOnSweepListener(mSweepListener);
mSweepTime2.addOnSweepListener(mSweepListener); mSweepTime2.addOnSweepListener(mSweepListener);
mSweepDataWarn.setVisibility(View.INVISIBLE);
mSweepDataLimit.setVisibility(View.INVISIBLE);
}
public void setChartColor(int stroke, int fill, int disabled) {
mSeries.setChartColor(stroke, fill, disabled);
} }
public void setListener(DataUsageChartListener listener) { public void setListener(DataUsageChartListener listener) {