More data usage chart iteration, app details.
Moved app details back into single Fragment to support animations and template tabs. Show the network in background behind app details chart series to match designs. Clamping sweeps at axis boundaries. Bug: 4813014, 4598460, 4818029 Change-Id: I72c0b21ee1d595e4da31d293ae0dab9e801041f3
Before Width: | Height: | Size: 851 B After Width: | Height: | Size: 857 B |
Before Width: | Height: | Size: 790 B After Width: | Height: | Size: 795 B |
Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 707 B |
Before Width: | Height: | Size: 833 B After Width: | Height: | Size: 838 B |
Before Width: | Height: | Size: 766 B After Width: | Height: | Size: 767 B |
Before Width: | Height: | Size: 799 B After Width: | Height: | Size: 806 B |
Before Width: | Height: | Size: 813 B After Width: | Height: | Size: 816 B |
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 850 B |
Before Width: | Height: | Size: 568 B After Width: | Height: | Size: 565 B |
Before Width: | Height: | Size: 526 B After Width: | Height: | Size: 524 B |
Before Width: | Height: | Size: 563 B After Width: | Height: | Size: 551 B |
Before Width: | Height: | Size: 591 B After Width: | Height: | Size: 554 B |
Before Width: | Height: | Size: 572 B After Width: | Height: | Size: 575 B |
Before Width: | Height: | Size: 535 B After Width: | Height: | Size: 533 B |
Before Width: | Height: | Size: 621 B After Width: | Height: | Size: 616 B |
Before Width: | Height: | Size: 616 B After Width: | Height: | Size: 581 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -17,6 +17,6 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||
|
||||
<item android:state_activated="true" android:drawable="@drawable/data_sweep_left_activated" />
|
||||
<item android:state_activated="false" android:drawable="@drawable/data_sweep_left_default" />
|
||||
<item android:state_activated="true" android:state_enabled="true" android:drawable="@drawable/data_sweep_left_activated" />
|
||||
<item android:drawable="@drawable/data_sweep_left_default" />
|
||||
</selector>
|
||||
|
@@ -17,6 +17,6 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||
|
||||
<item android:state_activated="true" android:drawable="@drawable/data_sweep_limit_activated" />
|
||||
<item android:state_activated="false" android:drawable="@drawable/data_sweep_limit_default" />
|
||||
<item android:state_activated="true" android:state_enabled="true" android:drawable="@drawable/data_sweep_limit_activated" />
|
||||
<item android:drawable="@drawable/data_sweep_limit_default" />
|
||||
</selector>
|
||||
|
@@ -17,6 +17,6 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||
|
||||
<item android:state_activated="true" android:drawable="@drawable/data_sweep_right_activated" />
|
||||
<item android:state_activated="false" android:drawable="@drawable/data_sweep_right_default" />
|
||||
<item android:state_activated="true" android:state_enabled="true" android:drawable="@drawable/data_sweep_right_activated" />
|
||||
<item android:drawable="@drawable/data_sweep_right_default" />
|
||||
</selector>
|
||||
|
@@ -17,6 +17,6 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||
|
||||
<item android:state_activated="true" android:drawable="@drawable/data_sweep_warning_activated" />
|
||||
<item android:state_activated="false" android:drawable="@drawable/data_sweep_warning_default" />
|
||||
<item android:state_activated="true" android:state_enabled="true" android:drawable="@drawable/data_sweep_warning_activated" />
|
||||
<item android:drawable="@drawable/data_sweep_warning_default" />
|
||||
</selector>
|
||||
|
@@ -17,6 +17,7 @@
|
||||
<com.android.settings.widget.DataUsageChartView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"
|
||||
android:id="@+id/chart"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="220dip"
|
||||
android:padding="16dip">
|
||||
@@ -40,11 +41,19 @@
|
||||
settings:fillColor="#c050ade5"
|
||||
settings:fillColorSecondary="#88566abc" />
|
||||
|
||||
<com.android.settings.widget.ChartNetworkSeriesView
|
||||
android:id="@+id/detail_series"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="left|bottom"
|
||||
settings:strokeColor="#d88d3a"
|
||||
settings:fillColor="#c0ba7f3e"
|
||||
settings:fillColorSecondary="#0000" />
|
||||
|
||||
<com.android.settings.widget.ChartSweepView
|
||||
android:id="@+id/sweep_left"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_horizontal"
|
||||
settings:sweepDrawable="@drawable/data_sweep_left"
|
||||
settings:followAxis="horizontal"
|
||||
settings:showLabel="false" />
|
||||
@@ -53,7 +62,6 @@
|
||||
android:id="@+id/sweep_right"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_horizontal"
|
||||
settings:sweepDrawable="@drawable/data_sweep_right"
|
||||
settings:followAxis="horizontal"
|
||||
settings:showLabel="false" />
|
||||
@@ -62,7 +70,6 @@
|
||||
android:id="@+id/sweep_limit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
settings:sweepDrawable="@drawable/data_sweep_limit"
|
||||
settings:followAxis="vertical"
|
||||
settings:showLabel="true" />
|
||||
@@ -71,7 +78,6 @@
|
||||
android:id="@+id/sweep_warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
settings:sweepDrawable="@drawable/data_sweep_warning"
|
||||
settings:followAxis="vertical"
|
||||
settings:showLabel="true" />
|
||||
|
@@ -14,43 +14,35 @@
|
||||
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
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/app_detail"
|
||||
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:id="@+id/app_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dip" />
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/text1"
|
||||
android:id="@+id/app_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dip" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/data_usage_app_settings"
|
||||
android:id="@+id/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:id="@+id/app_switches"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
@@ -20,7 +20,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/switches"
|
||||
android:id="@+id/network_switches"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
@@ -48,4 +48,7 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include layout="@layout/data_usage_chart" />
|
||||
<include layout="@layout/data_usage_detail" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@@ -1,309 +0,0 @@
|
||||
/*
|
||||
* 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_METERED_BACKGROUND;
|
||||
import static com.android.settings.DataUsageSummary.getHistoryBounds;
|
||||
|
||||
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.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.net.INetworkPolicyManager;
|
||||
import android.net.INetworkStatsService;
|
||||
import android.net.NetworkPolicyManager;
|
||||
import android.net.NetworkStats;
|
||||
import android.net.NetworkStatsHistory;
|
||||
import android.net.NetworkTemplate;
|
||||
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;
|
||||
|
||||
public static final String EXTRA_UID = "uid";
|
||||
public static final String EXTRA_NETWORK_TEMPLATE = "networkTemplate";
|
||||
|
||||
private int mUid;
|
||||
private NetworkTemplate mTemplate;
|
||||
|
||||
private Intent mAppSettingsIntent;
|
||||
|
||||
private static final String TAG_CONFIRM_RESTRICT = "confirmRestrict";
|
||||
|
||||
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 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();
|
||||
|
||||
updateBody();
|
||||
}
|
||||
|
||||
private void updateBody() {
|
||||
final PackageManager pm = getActivity().getPackageManager();
|
||||
|
||||
mUid = getArguments().getInt(EXTRA_UID);
|
||||
mTemplate = getArguments().getParcelable(EXTRA_NETWORK_TEMPLATE);
|
||||
|
||||
mTitle.setText(pm.getNameForUid(mUid));
|
||||
|
||||
// enable settings button when package provides it
|
||||
// TODO: target torwards entire UID instead of just first package
|
||||
final String[] packageNames = pm.getPackagesForUid(mUid);
|
||||
if (packageNames != null && packageNames.length > 0) {
|
||||
mAppSettingsIntent = new Intent(Intent.ACTION_MANAGE_NETWORK_USAGE);
|
||||
mAppSettingsIntent.setPackage(packageNames[0]);
|
||||
mAppSettingsIntent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
|
||||
final boolean matchFound = pm.resolveActivity(mAppSettingsIntent, 0) != null;
|
||||
mAppSettings.setEnabled(matchFound);
|
||||
|
||||
} else {
|
||||
mAppSettingsIntent = null;
|
||||
mAppSettings.setEnabled(false);
|
||||
}
|
||||
|
||||
try {
|
||||
// load stats for current uid and template
|
||||
// TODO: read template from extras
|
||||
mHistory = mStatsService.getHistoryForUid(mTemplate, mUid, NetworkStats.TAG_NONE);
|
||||
} catch (RemoteException e) {
|
||||
// since we can't do much without 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();
|
||||
|
||||
final Context context = getActivity();
|
||||
if (NetworkPolicyManager.isUidValidForPolicy(context, mUid)) {
|
||||
mRestrictBackgroundView.setVisibility(View.VISIBLE);
|
||||
|
||||
final int uidPolicy;
|
||||
try {
|
||||
uidPolicy = mPolicyService.getUidPolicy(mUid);
|
||||
} catch (RemoteException e) {
|
||||
// since we can't do much without policy, we bail hard.
|
||||
throw new RuntimeException("problem reading network policy", e);
|
||||
}
|
||||
|
||||
// update policy checkbox
|
||||
final boolean restrictBackground = (uidPolicy & POLICY_REJECT_METERED_BACKGROUND) != 0;
|
||||
mRestrictBackground.setChecked(restrictBackground);
|
||||
|
||||
// kick preference views so they rebind from changes above
|
||||
refreshPreferenceViews();
|
||||
|
||||
} else {
|
||||
mRestrictBackgroundView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
private void setRestrictBackground(boolean restrictBackground) {
|
||||
if (LOGD) Log.d(TAG, "setRestrictBackground()");
|
||||
try {
|
||||
mPolicyService.setUidPolicy(
|
||||
mUid, restrictBackground ? POLICY_REJECT_METERED_BACKGROUND : POLICY_NONE);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException("unable to save policy", e);
|
||||
}
|
||||
|
||||
mRestrictBackground.setChecked(restrictBackground);
|
||||
refreshPreferenceViews();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
startActivity(mAppSettingsIntent);
|
||||
}
|
||||
};
|
||||
|
||||
private OnClickListener mRestrictBackgroundListener = new OnClickListener() {
|
||||
/** {@inheritDoc} */
|
||||
public void onClick(View v) {
|
||||
final boolean restrictBackground = !mRestrictBackground.isChecked();
|
||||
|
||||
if (restrictBackground) {
|
||||
// enabling restriction; show confirmation dialog which
|
||||
// eventually calls setRestrictBackground() once user confirms.
|
||||
ConfirmRestrictFragment.show(DataUsageAppDetail.this);
|
||||
} else {
|
||||
setRestrictBackground(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dialog to request user confirmation before setting
|
||||
* {@link #POLICY_REJECT_METERED_BACKGROUND}.
|
||||
*/
|
||||
public static class ConfirmRestrictFragment extends DialogFragment {
|
||||
public static void show(DataUsageAppDetail parent) {
|
||||
final ConfirmRestrictFragment dialog = new ConfirmRestrictFragment();
|
||||
dialog.setTargetFragment(parent, 0);
|
||||
dialog.show(parent.getFragmentManager(), TAG_CONFIRM_RESTRICT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Context context = getActivity();
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.data_usage_app_restrict_dialog_title);
|
||||
builder.setMessage(R.string.data_usage_app_restrict_dialog);
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
final DataUsageAppDetail target = (DataUsageAppDetail) getTargetFragment();
|
||||
if (target != null) {
|
||||
target.setRestrictBackground(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -21,6 +21,8 @@ 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.POLICY_NONE;
|
||||
import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND;
|
||||
import static android.net.NetworkPolicyManager.computeLastCycleBoundary;
|
||||
import static android.net.NetworkPolicyManager.computeNextCycleBoundary;
|
||||
import static android.net.NetworkTemplate.MATCH_MOBILE_3G_LOWER;
|
||||
@@ -29,10 +31,12 @@ import static android.net.NetworkTemplate.MATCH_MOBILE_ALL;
|
||||
import static android.net.NetworkTemplate.MATCH_WIFI;
|
||||
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
import android.animation.LayoutTransition;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.DialogFragment;
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
@@ -54,7 +58,6 @@ 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;
|
||||
@@ -66,12 +69,14 @@ 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.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.AdapterView.OnItemSelectedListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
@@ -106,8 +111,6 @@ 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";
|
||||
@@ -116,6 +119,8 @@ public class DataUsageSummary extends Fragment {
|
||||
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 String TAG_CONFIRM_RESTRICT = "confirmRestrict";
|
||||
private static final String TAG_APP_DETAILS = "appDetails";
|
||||
|
||||
private static final long KB_IN_BYTES = 1024;
|
||||
private static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
|
||||
@@ -135,25 +140,41 @@ public class DataUsageSummary extends Fragment {
|
||||
private ListView mListView;
|
||||
private DataUsageAdapter mAdapter;
|
||||
|
||||
private View mHeader;
|
||||
private LinearLayout mSwitches;
|
||||
private ViewGroup mHeader;
|
||||
|
||||
private LinearLayout mNetworkSwitches;
|
||||
private Switch mDataEnabled;
|
||||
private CheckBox mDisableAtLimit;
|
||||
private View mDataEnabledView;
|
||||
private CheckBox mDisableAtLimit;
|
||||
private View mDisableAtLimitView;
|
||||
|
||||
private DataUsageChartView mChart;
|
||||
|
||||
private Spinner mCycleSpinner;
|
||||
private CycleAdapter mCycleAdapter;
|
||||
|
||||
private DataUsageChartView mChart;
|
||||
|
||||
private View mAppDetail;
|
||||
private TextView mAppTitle;
|
||||
private TextView mAppSubtitle;
|
||||
private Button mAppSettings;
|
||||
|
||||
private LinearLayout mAppSwitches;
|
||||
private CheckBox mAppRestrict;
|
||||
private View mAppRestrictView;
|
||||
|
||||
private boolean mShowWifi = false;
|
||||
|
||||
private NetworkTemplate mTemplate = null;
|
||||
|
||||
private static final int UID_NONE = -1;
|
||||
private int mUid = UID_NONE;
|
||||
|
||||
private Intent mAppSettingsIntent;
|
||||
|
||||
private NetworkPolicyEditor mPolicyEditor;
|
||||
|
||||
private NetworkStatsHistory mHistory;
|
||||
private NetworkStatsHistory mDetailHistory;
|
||||
|
||||
private String mIntentTab = null;
|
||||
|
||||
@@ -194,30 +215,57 @@ public class DataUsageSummary extends Fragment {
|
||||
mTabHost.setup();
|
||||
mTabHost.setOnTabChangedListener(mTabListener);
|
||||
|
||||
mHeader = inflater.inflate(R.layout.data_usage_header, mListView, false);
|
||||
mHeader = (ViewGroup) inflater.inflate(R.layout.data_usage_header, mListView, false);
|
||||
mListView.addHeaderView(mHeader, null, false);
|
||||
|
||||
{
|
||||
// bind network switches
|
||||
mNetworkSwitches = (LinearLayout) mHeader.findViewById(R.id.network_switches);
|
||||
|
||||
mDataEnabled = new Switch(inflater.getContext());
|
||||
mDataEnabledView = inflatePreference(inflater, mSwitches, mDataEnabled);
|
||||
mDataEnabledView = inflatePreference(inflater, mNetworkSwitches, mDataEnabled);
|
||||
mDataEnabled.setOnCheckedChangeListener(mDataEnabledListener);
|
||||
mNetworkSwitches.addView(mDataEnabledView);
|
||||
|
||||
mDisableAtLimit = new CheckBox(inflater.getContext());
|
||||
mDisableAtLimit.setClickable(false);
|
||||
mDisableAtLimitView = inflatePreference(inflater, mSwitches, mDisableAtLimit);
|
||||
mDisableAtLimitView = inflatePreference(inflater, mNetworkSwitches, mDisableAtLimit);
|
||||
mDisableAtLimitView.setOnClickListener(mDisableAtLimitListener);
|
||||
mNetworkSwitches.addView(mDisableAtLimitView);
|
||||
}
|
||||
|
||||
mSwitches = (LinearLayout) mHeader.findViewById(R.id.switches);
|
||||
mSwitches.addView(mDataEnabledView);
|
||||
mSwitches.addView(mDisableAtLimitView);
|
||||
|
||||
// bind cycle dropdown
|
||||
mCycleSpinner = (Spinner) mHeader.findViewById(R.id.cycles);
|
||||
mCycleAdapter = new CycleAdapter(context);
|
||||
mCycleSpinner.setAdapter(mCycleAdapter);
|
||||
mCycleSpinner.setOnItemSelectedListener(mCycleListener);
|
||||
|
||||
mChart = (DataUsageChartView) inflater.inflate(R.layout.data_usage_chart, mListView, false);
|
||||
mChart = (DataUsageChartView) mHeader.findViewById(R.id.chart);
|
||||
mChart.setListener(mChartListener);
|
||||
mListView.addHeaderView(mChart, null, false);
|
||||
|
||||
{
|
||||
// bind app detail controls
|
||||
mAppDetail = view.findViewById(R.id.app_detail);
|
||||
mAppTitle = (TextView) view.findViewById(R.id.app_title);
|
||||
mAppSubtitle = (TextView) view.findViewById(R.id.app_subtitle);
|
||||
mAppSwitches = (LinearLayout) view.findViewById(R.id.app_switches);
|
||||
|
||||
mAppSettings = (Button) view.findViewById(R.id.app_settings);
|
||||
mAppSettings.setOnClickListener(mAppSettingsListener);
|
||||
|
||||
mAppRestrict = new CheckBox(inflater.getContext());
|
||||
mAppRestrict.setClickable(false);
|
||||
mAppRestrictView = inflatePreference(inflater, mAppSwitches, mAppRestrict);
|
||||
setPreferenceTitle(mAppRestrictView, R.string.data_usage_app_restrict_background);
|
||||
setPreferenceSummary(
|
||||
mAppRestrictView, R.string.data_usage_app_restrict_background_summary);
|
||||
mAppRestrictView.setOnClickListener(mAppRestrictListener);
|
||||
mAppSwitches.addView(mAppRestrictView);
|
||||
}
|
||||
|
||||
// TODO: tweak these transitions
|
||||
final LayoutTransition transition = new LayoutTransition();
|
||||
mHeader.setLayoutTransition(transition);
|
||||
|
||||
mAdapter = new DataUsageAdapter();
|
||||
mListView.setOnItemClickListener(mListListener);
|
||||
@@ -433,6 +481,7 @@ public class DataUsageSummary extends Fragment {
|
||||
mChart.bindNetworkStats(mHistory);
|
||||
|
||||
updatePolicy(true);
|
||||
updateAppDetail();
|
||||
|
||||
// force scroll to top of body
|
||||
mListView.smoothScrollToPosition(0);
|
||||
@@ -440,6 +489,84 @@ public class DataUsageSummary extends Fragment {
|
||||
mBinding = false;
|
||||
}
|
||||
|
||||
private boolean isAppDetailMode() {
|
||||
return mUid != UID_NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UID details panels to match {@link #mUid}, showing or hiding them
|
||||
* depending on {@link #isAppDetailMode()}.
|
||||
*/
|
||||
private void updateAppDetail() {
|
||||
if (isAppDetailMode()) {
|
||||
mAppDetail.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mAppDetail.setVisibility(View.GONE);
|
||||
|
||||
// hide detail stats when not in detail mode
|
||||
mChart.bindDetailNetworkStats(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove warning/limit sweeps while in detail mode
|
||||
mChart.bindNetworkPolicy(null);
|
||||
|
||||
final PackageManager pm = getActivity().getPackageManager();
|
||||
mAppTitle.setText(pm.getNameForUid(mUid));
|
||||
|
||||
// enable settings button when package provides it
|
||||
// TODO: target torwards entire UID instead of just first package
|
||||
final String[] packageNames = pm.getPackagesForUid(mUid);
|
||||
if (packageNames != null && packageNames.length > 0) {
|
||||
mAppSettingsIntent = new Intent(Intent.ACTION_MANAGE_NETWORK_USAGE);
|
||||
mAppSettingsIntent.setPackage(packageNames[0]);
|
||||
mAppSettingsIntent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
|
||||
final boolean matchFound = pm.resolveActivity(mAppSettingsIntent, 0) != null;
|
||||
mAppSettings.setEnabled(matchFound);
|
||||
|
||||
} else {
|
||||
mAppSettingsIntent = null;
|
||||
mAppSettings.setEnabled(false);
|
||||
}
|
||||
|
||||
try {
|
||||
// load stats for current uid and template
|
||||
// TODO: read template from extras
|
||||
mDetailHistory = mStatsService.getHistoryForUid(mTemplate, mUid, NetworkStats.TAG_NONE);
|
||||
} catch (RemoteException e) {
|
||||
// since we can't do much without 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.bindDetailNetworkStats(mDetailHistory);
|
||||
|
||||
updateDetailData();
|
||||
|
||||
final Context context = getActivity();
|
||||
if (NetworkPolicyManager.isUidValidForPolicy(context, mUid)) {
|
||||
mAppRestrictView.setVisibility(View.VISIBLE);
|
||||
|
||||
final int uidPolicy;
|
||||
try {
|
||||
uidPolicy = mPolicyService.getUidPolicy(mUid);
|
||||
} catch (RemoteException e) {
|
||||
// since we can't do much without policy, we bail hard.
|
||||
throw new RuntimeException("problem reading network policy", e);
|
||||
}
|
||||
|
||||
// update policy checkbox
|
||||
final boolean restrictBackground = (uidPolicy & POLICY_REJECT_METERED_BACKGROUND) != 0;
|
||||
mAppRestrict.setChecked(restrictBackground);
|
||||
|
||||
} else {
|
||||
mAppRestrictView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void setPolicyCycleDay(int cycleDay) {
|
||||
if (LOGD) Log.d(TAG, "setPolicyCycleDay()");
|
||||
mPolicyEditor.setPolicyCycleDay(mTemplate, cycleDay);
|
||||
@@ -458,11 +585,30 @@ public class DataUsageSummary extends Fragment {
|
||||
updatePolicy(false);
|
||||
}
|
||||
|
||||
private void setAppRestrictBackground(boolean restrictBackground) {
|
||||
if (LOGD) Log.d(TAG, "setRestrictBackground()");
|
||||
try {
|
||||
mPolicyService.setUidPolicy(
|
||||
mUid, restrictBackground ? POLICY_REJECT_METERED_BACKGROUND : POLICY_NONE);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException("unable to save policy", e);
|
||||
}
|
||||
|
||||
mAppRestrict.setChecked(restrictBackground);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update chart sweeps and cycle list to reflect {@link NetworkPolicy} for
|
||||
* current {@link #mTemplate}.
|
||||
*/
|
||||
private void updatePolicy(boolean refreshCycle) {
|
||||
if (isAppDetailMode()) {
|
||||
mNetworkSwitches.setVisibility(View.GONE);
|
||||
// we fall through to update cycle list for detail mode
|
||||
} else {
|
||||
mNetworkSwitches.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
final NetworkPolicy policy = mPolicyEditor.getPolicy(mTemplate);
|
||||
|
||||
// reflect policy limit in checkbox
|
||||
@@ -572,18 +718,34 @@ public class DataUsageSummary extends Fragment {
|
||||
}
|
||||
};
|
||||
|
||||
private View.OnClickListener mAppRestrictListener = new View.OnClickListener() {
|
||||
/** {@inheritDoc} */
|
||||
public void onClick(View v) {
|
||||
final boolean restrictBackground = !mAppRestrict.isChecked();
|
||||
|
||||
if (restrictBackground) {
|
||||
// enabling restriction; show confirmation dialog which
|
||||
// eventually calls setRestrictBackground() once user confirms.
|
||||
ConfirmRestrictFragment.show(DataUsageSummary.this);
|
||||
} else {
|
||||
setAppRestrictBackground(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private OnClickListener mAppSettingsListener = new OnClickListener() {
|
||||
/** {@inheritDoc} */
|
||||
public void onClick(View v) {
|
||||
// TODO: target torwards entire UID instead of just first package
|
||||
startActivity(mAppSettingsIntent);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
AppDetailsFragment.show(DataUsageSummary.this, app.uid);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -621,12 +783,30 @@ public class DataUsageSummary extends Fragment {
|
||||
};
|
||||
|
||||
/**
|
||||
* Update {@link #mAdapter} with sorted list of applications data usage,
|
||||
* based on current inspection from {@link #mChart}.
|
||||
* Update details based on {@link #mChart} inspection range depending on
|
||||
* current mode. In network mode, updates {@link #mAdapter} with sorted list
|
||||
* of applications data usage, and when {@link #isAppDetailMode()} update
|
||||
* app details.
|
||||
*/
|
||||
private void updateDetailData() {
|
||||
if (LOGD) Log.d(TAG, "updateDetailData()");
|
||||
|
||||
if (isAppDetailMode()) {
|
||||
if (mDetailHistory != null) {
|
||||
final Context context = mChart.getContext();
|
||||
final long[] range = mChart.getInspectRange();
|
||||
final long[] total = mDetailHistory.getTotalData(range[0], range[1], null);
|
||||
final long totalCombined = total[0] + total[1];
|
||||
mAppSubtitle.setText(Formatter.formatFileSize(context, totalCombined));
|
||||
}
|
||||
|
||||
// clear any existing app list details
|
||||
mAdapter.bindStats(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise kick off task to update list
|
||||
new AsyncTask<Void, Void, NetworkStats>() {
|
||||
@Override
|
||||
protected NetworkStats doInBackground(Void... params) {
|
||||
@@ -753,9 +933,13 @@ public class DataUsageSummary extends Fragment {
|
||||
public static class DataUsageAdapter extends BaseAdapter {
|
||||
private ArrayList<AppUsageItem> mItems = Lists.newArrayList();
|
||||
|
||||
/**
|
||||
* Bind the given {@link NetworkStats}, or {@code null} to clear list.
|
||||
*/
|
||||
public void bindStats(NetworkStats stats) {
|
||||
mItems.clear();
|
||||
|
||||
if (stats != null) {
|
||||
for (int i = 0; i < stats.size; i++) {
|
||||
final long total = stats.rx[i] + stats.tx[i];
|
||||
final AppUsageItem item = new AppUsageItem();
|
||||
@@ -763,6 +947,7 @@ public class DataUsageSummary extends Fragment {
|
||||
item.total = total;
|
||||
mItems.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(mItems);
|
||||
notifyDataSetChanged();
|
||||
@@ -805,6 +990,44 @@ public class DataUsageSummary extends Fragment {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty {@link Fragment} that controls display of UID details in
|
||||
* {@link DataUsageSummary}.
|
||||
*/
|
||||
public static class AppDetailsFragment extends Fragment {
|
||||
public static final String EXTRA_UID = "uid";
|
||||
|
||||
public static void show(DataUsageSummary parent, int uid) {
|
||||
final Bundle args = new Bundle();
|
||||
args.putInt(EXTRA_UID, uid);
|
||||
|
||||
final AppDetailsFragment fragment = new AppDetailsFragment();
|
||||
fragment.setArguments(args);
|
||||
fragment.setTargetFragment(parent, 0);
|
||||
|
||||
final FragmentTransaction ft = parent.getFragmentManager().beginTransaction();
|
||||
ft.add(fragment, TAG_APP_DETAILS);
|
||||
ft.addToBackStack(TAG_APP_DETAILS);
|
||||
ft.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
final DataUsageSummary target = (DataUsageSummary) getTargetFragment();
|
||||
target.mUid = getArguments().getInt(EXTRA_UID);
|
||||
target.updateBody();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
final DataUsageSummary target = (DataUsageSummary) getTargetFragment();
|
||||
target.mUid = UID_NONE;
|
||||
target.updateBody();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog to request user confirmation before setting
|
||||
* {@link NetworkPolicy#limitBytes}.
|
||||
@@ -975,12 +1198,45 @@ public class DataUsageSummary extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog to request user confirmation before setting
|
||||
* {@link #POLICY_REJECT_METERED_BACKGROUND}.
|
||||
*/
|
||||
public static class ConfirmRestrictFragment extends DialogFragment {
|
||||
public static void show(DataUsageSummary parent) {
|
||||
final ConfirmRestrictFragment dialog = new ConfirmRestrictFragment();
|
||||
dialog.setTargetFragment(parent, 0);
|
||||
dialog.show(parent.getFragmentManager(), TAG_CONFIRM_RESTRICT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Context context = getActivity();
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.data_usage_app_restrict_dialog_title);
|
||||
builder.setMessage(R.string.data_usage_app_restrict_dialog);
|
||||
|
||||
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.setAppRestrictBackground(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
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);
|
||||
final int networkTemplate = intent.getIntExtra(EXTRA_NETWORK_TEMPLATE, MATCH_MOBILE_ALL);
|
||||
switch (networkTemplate) {
|
||||
case MATCH_MOBILE_3G_LOWER:
|
||||
return TAB_3G;
|
||||
@@ -1083,4 +1339,14 @@ public class DataUsageSummary extends Fragment {
|
||||
title.setText(resId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link android.R.id#summary} for a preference view inflated with
|
||||
* {@link #inflatePreference(LayoutInflater, ViewGroup, View)}.
|
||||
*/
|
||||
private static void setPreferenceSummary(View parent, int resId) {
|
||||
final TextView summary = (TextView) parent.findViewById(android.R.id.summary);
|
||||
summary.setVisibility(View.VISIBLE);
|
||||
summary.setText(resId);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -16,6 +16,8 @@
|
||||
|
||||
package com.android.settings.net;
|
||||
|
||||
import static android.net.NetworkPolicy.LIMIT_DISABLED;
|
||||
import static android.net.NetworkPolicy.WARNING_DISABLED;
|
||||
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;
|
||||
@@ -51,6 +53,14 @@ public class NetworkPolicyEditor {
|
||||
final NetworkPolicy[] policies = mPolicyService.getNetworkPolicies();
|
||||
mPolicies.clear();
|
||||
for (NetworkPolicy policy : policies) {
|
||||
// TODO: find better place to clamp these
|
||||
if (policy.limitBytes < -1) {
|
||||
policy.limitBytes = LIMIT_DISABLED;
|
||||
}
|
||||
if (policy.warningBytes < -1) {
|
||||
policy.warningBytes = WARNING_DISABLED;
|
||||
}
|
||||
|
||||
mPolicies.add(policy);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
|
@@ -75,6 +75,7 @@ public class ChartNetworkSeriesView extends View {
|
||||
R.styleable.ChartNetworkSeriesView_fillColorSecondary, Color.RED);
|
||||
|
||||
setChartColor(stroke, fill, fillSecondary);
|
||||
setWillNotDraw(false);
|
||||
|
||||
a.recycle();
|
||||
|
||||
@@ -110,8 +111,13 @@ public class ChartNetworkSeriesView extends View {
|
||||
|
||||
mPathStroke.reset();
|
||||
mPathFill.reset();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the range to paint with {@link #mPaintFill}, leaving the remaining
|
||||
* area to be painted with {@link #mPaintFillSecondary}.
|
||||
*/
|
||||
public void setPrimaryRange(long left, long right) {
|
||||
mPrimaryLeft = left;
|
||||
mPrimaryRight = right;
|
||||
@@ -190,18 +196,21 @@ public class ChartNetworkSeriesView extends View {
|
||||
protected void onDraw(Canvas canvas) {
|
||||
int save;
|
||||
|
||||
final float primaryLeftPoint = mHoriz.convertToPoint(mPrimaryLeft);
|
||||
final float primaryRightPoint = mHoriz.convertToPoint(mPrimaryRight);
|
||||
|
||||
save = canvas.save();
|
||||
canvas.clipRect(0, 0, mPrimaryLeft, getHeight());
|
||||
canvas.clipRect(0, 0, primaryLeftPoint, getHeight());
|
||||
canvas.drawPath(mPathFill, mPaintFillSecondary);
|
||||
canvas.restoreToCount(save);
|
||||
|
||||
save = canvas.save();
|
||||
canvas.clipRect(mPrimaryRight, 0, getWidth(), getHeight());
|
||||
canvas.clipRect(primaryRightPoint, 0, getWidth(), getHeight());
|
||||
canvas.drawPath(mPathFill, mPaintFillSecondary);
|
||||
canvas.restoreToCount(save);
|
||||
|
||||
save = canvas.save();
|
||||
canvas.clipRect(mPrimaryLeft, 0, mPrimaryRight, getHeight());
|
||||
canvas.clipRect(primaryLeftPoint, 0, primaryRightPoint, getHeight());
|
||||
canvas.drawPath(mPathFill, mPaintFill);
|
||||
canvas.drawPath(mPathStroke, mPaintStroke);
|
||||
canvas.restoreToCount(save);
|
||||
|
@@ -39,6 +39,8 @@ public class ChartSweepView extends FrameLayout {
|
||||
// TODO: paint label when requested
|
||||
|
||||
private Drawable mSweep;
|
||||
private Rect mSweepMargins = new Rect();
|
||||
|
||||
private int mFollowAxis;
|
||||
private boolean mShowLabel;
|
||||
|
||||
@@ -88,8 +90,28 @@ public class ChartSweepView extends FrameLayout {
|
||||
return mFollowAxis;
|
||||
}
|
||||
|
||||
public void getExtraMargins(Rect rect) {
|
||||
mSweep.getPadding(rect);
|
||||
/**
|
||||
* Return margins of {@link #setSweepDrawable(Drawable)}, indicating how the
|
||||
* sweep should be displayed around a content region.
|
||||
*/
|
||||
public Rect getSweepMargins() {
|
||||
return mSweepMargins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of pixels that the "target" area is inset from the
|
||||
* {@link View} edge, along the current {@link #setFollowAxis(int)}.
|
||||
*/
|
||||
public float getTargetInset() {
|
||||
if (mFollowAxis == VERTICAL) {
|
||||
final float targetHeight = mSweep.getIntrinsicHeight() - mSweepMargins.top
|
||||
- mSweepMargins.bottom;
|
||||
return mSweepMargins.top + (targetHeight / 2);
|
||||
} else {
|
||||
final float targetWidth = mSweep.getIntrinsicWidth() - mSweepMargins.left
|
||||
- mSweepMargins.right;
|
||||
return mSweepMargins.left + (targetWidth / 2);
|
||||
}
|
||||
}
|
||||
|
||||
public void addOnSweepListener(OnSweepListener listener) {
|
||||
@@ -115,6 +137,7 @@ public class ChartSweepView extends FrameLayout {
|
||||
}
|
||||
sweep.setVisible(getVisibility() == VISIBLE, false);
|
||||
mSweep = sweep;
|
||||
sweep.getPadding(mSweepMargins);
|
||||
} else {
|
||||
mSweep = null;
|
||||
}
|
||||
@@ -175,33 +198,51 @@ public class ChartSweepView extends FrameLayout {
|
||||
final View parent = (View) getParent();
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
|
||||
// only start tracking when in sweet spot
|
||||
final boolean accept;
|
||||
if (mFollowAxis == VERTICAL) {
|
||||
accept = event.getX() > getWidth() - (mSweepMargins.right * 2);
|
||||
} else {
|
||||
accept = event.getY() > getHeight() - (mSweepMargins.bottom * 2);
|
||||
}
|
||||
|
||||
if (accept) {
|
||||
mTracking = event.copy();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
getParent().requestDisallowInterceptTouchEvent(true);
|
||||
|
||||
final Rect sweepMargins = mSweepMargins;
|
||||
|
||||
// content area of parent
|
||||
final Rect parentContent = new Rect(parent.getPaddingLeft(), parent.getPaddingTop(),
|
||||
parent.getWidth() - parent.getPaddingRight(),
|
||||
parent.getHeight() - parent.getPaddingBottom());
|
||||
|
||||
if (mFollowAxis == VERTICAL) {
|
||||
final float chartHeight = parent.getHeight() - parent.getPaddingTop()
|
||||
- parent.getPaddingBottom();
|
||||
final float translationY = MathUtils.constrain(
|
||||
event.getRawY() - mTracking.getRawY(), -getTop(),
|
||||
chartHeight - getTop());
|
||||
setTranslationY(translationY);
|
||||
final float point = (getTop() + getTranslationY() + (getHeight() / 2))
|
||||
- parent.getPaddingTop();
|
||||
mValue = mAxis.convertToValue(point);
|
||||
final float currentTargetY = getTop() + getTargetInset();
|
||||
final float requestedTargetY = currentTargetY
|
||||
+ (event.getRawY() - mTracking.getRawY());
|
||||
final float clampedTargetY = MathUtils.constrain(
|
||||
requestedTargetY, parentContent.top, parentContent.bottom);
|
||||
setTranslationY(clampedTargetY - currentTargetY);
|
||||
|
||||
mValue = mAxis.convertToValue(clampedTargetY - parentContent.top);
|
||||
dispatchOnSweep(false);
|
||||
} else {
|
||||
final float chartWidth = parent.getWidth() - parent.getPaddingLeft()
|
||||
- parent.getPaddingRight();
|
||||
final float translationX = MathUtils.constrain(
|
||||
event.getRawX() - mTracking.getRawX(), -getLeft(),
|
||||
chartWidth - getLeft());
|
||||
setTranslationX(translationX);
|
||||
final float point = (getLeft() + getTranslationX() + (getWidth() / 2))
|
||||
- parent.getPaddingLeft();
|
||||
mValue = mAxis.convertToValue(point);
|
||||
final float currentTargetX = getLeft() + getTargetInset();
|
||||
final float requestedTargetX = currentTargetX
|
||||
+ (event.getRawX() - mTracking.getRawX());
|
||||
final float clampedTargetX = MathUtils.constrain(
|
||||
requestedTargetX, parentContent.left, parentContent.right);
|
||||
setTranslationX(clampedTargetX - currentTargetX);
|
||||
|
||||
mValue = mAxis.convertToValue(clampedTargetX - parentContent.left);
|
||||
dispatchOnSweep(false);
|
||||
}
|
||||
return true;
|
||||
|
@@ -36,6 +36,8 @@ public class ChartView extends FrameLayout {
|
||||
|
||||
// TODO: extend something that supports two-dimensional scrolling
|
||||
|
||||
private static final int SWEEP_GRAVITY = Gravity.TOP | Gravity.LEFT;
|
||||
|
||||
ChartAxis mHoriz;
|
||||
ChartAxis mVert;
|
||||
|
||||
@@ -74,7 +76,6 @@ public class ChartView extends FrameLayout {
|
||||
|
||||
final Rect parentRect = new Rect();
|
||||
final Rect childRect = new Rect();
|
||||
final Rect extraMargins = new Rect();
|
||||
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
final View child = getChildAt(i);
|
||||
@@ -91,21 +92,23 @@ public class ChartView extends FrameLayout {
|
||||
} else if (child instanceof ChartSweepView) {
|
||||
// sweep is always placed along specific dimension
|
||||
final ChartSweepView sweep = (ChartSweepView) child;
|
||||
final Rect sweepMargins = sweep.getSweepMargins();
|
||||
final float point = sweep.getPoint();
|
||||
sweep.getExtraMargins(extraMargins);
|
||||
|
||||
if (sweep.getFollowAxis() == ChartSweepView.HORIZONTAL) {
|
||||
parentRect.left = parentRect.right = (int) point + getPaddingLeft();
|
||||
parentRect.top -= extraMargins.top;
|
||||
parentRect.bottom += extraMargins.bottom;
|
||||
Gravity.apply(params.gravity, child.getMeasuredWidth(), parentRect.height(),
|
||||
parentRect.left = parentRect.right =
|
||||
(int) (sweep.getPoint() - sweep.getTargetInset()) + getPaddingLeft();
|
||||
parentRect.top -= sweepMargins.top;
|
||||
parentRect.bottom += sweepMargins.bottom;
|
||||
Gravity.apply(SWEEP_GRAVITY, child.getMeasuredWidth(), parentRect.height(),
|
||||
parentRect, childRect);
|
||||
|
||||
} else {
|
||||
parentRect.top = parentRect.bottom = (int) point + getPaddingTop();
|
||||
parentRect.left -= extraMargins.left;
|
||||
parentRect.right += extraMargins.right;
|
||||
Gravity.apply(params.gravity, parentRect.width(), child.getMeasuredHeight(),
|
||||
parentRect.top = parentRect.bottom =
|
||||
(int) (sweep.getPoint() - sweep.getTargetInset()) + getPaddingTop();
|
||||
parentRect.left -= sweepMargins.left;
|
||||
parentRect.right += sweepMargins.right;
|
||||
Gravity.apply(SWEEP_GRAVITY, parentRect.width(), child.getMeasuredHeight(),
|
||||
parentRect, childRect);
|
||||
}
|
||||
}
|
||||
|
@@ -38,10 +38,10 @@ public class DataUsageChartView extends ChartView {
|
||||
private static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
|
||||
|
||||
// TODO: enforce that sweeps cant cross each other
|
||||
// TODO: limit sweeps at graph boundaries
|
||||
|
||||
private ChartGridView mGrid;
|
||||
private ChartNetworkSeriesView mSeries;
|
||||
private ChartNetworkSeriesView mDetailSeries;
|
||||
|
||||
private ChartSweepView mSweepLeft;
|
||||
private ChartSweepView mSweepRight;
|
||||
@@ -75,6 +75,8 @@ public class DataUsageChartView extends ChartView {
|
||||
|
||||
mGrid = (ChartGridView) findViewById(R.id.grid);
|
||||
mSeries = (ChartNetworkSeriesView) findViewById(R.id.series);
|
||||
mDetailSeries = (ChartNetworkSeriesView) findViewById(R.id.detail_series);
|
||||
mDetailSeries.setVisibility(View.GONE);
|
||||
|
||||
mSweepLeft = (ChartSweepView) findViewById(R.id.sweep_left);
|
||||
mSweepRight = (ChartSweepView) findViewById(R.id.sweep_right);
|
||||
@@ -89,6 +91,7 @@ public class DataUsageChartView extends ChartView {
|
||||
// tell everyone about our axis
|
||||
mGrid.init(mHoriz, mVert);
|
||||
mSeries.init(mHoriz, mVert);
|
||||
mDetailSeries.init(mHoriz, mVert);
|
||||
mSweepLeft.init(mHoriz);
|
||||
mSweepRight.init(mHoriz);
|
||||
mSweepWarning.init(mVert);
|
||||
@@ -97,27 +100,21 @@ public class DataUsageChartView extends ChartView {
|
||||
setActivated(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActivated(boolean activated) {
|
||||
super.setActivated(activated);
|
||||
|
||||
mSweepLeft.setEnabled(activated);
|
||||
mSweepRight.setEnabled(activated);
|
||||
mSweepWarning.setEnabled(activated);
|
||||
mSweepLimit.setEnabled(activated);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setChartColor(int stroke, int fill, int disabled) {
|
||||
mSeries.setChartColor(stroke, fill, disabled);
|
||||
}
|
||||
|
||||
public void setListener(DataUsageChartListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void bindNetworkStats(NetworkStatsHistory stats) {
|
||||
mSeries.bindNetworkStats(stats);
|
||||
updatePrimaryRange();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
public void bindDetailNetworkStats(NetworkStatsHistory stats) {
|
||||
mDetailSeries.bindNetworkStats(stats);
|
||||
mDetailSeries.setVisibility(stats != null ? View.VISIBLE : View.GONE);
|
||||
updatePrimaryRange();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
public void bindNetworkPolicy(NetworkPolicy policy) {
|
||||
@@ -146,22 +143,11 @@ public class DataUsageChartView extends ChartView {
|
||||
}
|
||||
|
||||
requestLayout();
|
||||
|
||||
// TODO: eventually remove this; was to work around lack of sweep clamping
|
||||
if (policy.limitBytes < -1 || policy.limitBytes > 5 * GB_IN_BYTES) {
|
||||
policy.limitBytes = 5 * GB_IN_BYTES;
|
||||
mLimitListener.onSweep(mSweepLimit, true);
|
||||
}
|
||||
if (policy.warningBytes < -1 || policy.warningBytes > 5 * GB_IN_BYTES) {
|
||||
policy.warningBytes = 4 * GB_IN_BYTES;
|
||||
mWarningListener.onSweep(mSweepWarning, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private OnSweepListener mSweepListener = new OnSweepListener() {
|
||||
public void onSweep(ChartSweepView sweep, boolean sweepDone) {
|
||||
mSeries.setPrimaryRange(mSweepLeft.getValue(), mSweepRight.getValue());
|
||||
updatePrimaryRange();
|
||||
|
||||
// update detail list only when done sweeping
|
||||
if (sweepDone && mListener != null) {
|
||||
@@ -236,13 +222,26 @@ public class DataUsageChartView extends ChartView {
|
||||
|
||||
mSweepLeft.setValue(sweepMin);
|
||||
mSweepRight.setValue(sweepMax);
|
||||
mSeries.setPrimaryRange(sweepMin, sweepMax);
|
||||
updatePrimaryRange();
|
||||
|
||||
requestLayout();
|
||||
mSeries.generatePath();
|
||||
mSeries.invalidate();
|
||||
}
|
||||
|
||||
private void updatePrimaryRange() {
|
||||
final long left = mSweepLeft.getValue();
|
||||
final long right = mSweepRight.getValue();
|
||||
|
||||
// prefer showing primary range on detail series, when available
|
||||
if (mDetailSeries.getVisibility() == View.VISIBLE) {
|
||||
mDetailSeries.setPrimaryRange(left, right);
|
||||
mSeries.setPrimaryRange(0, 0);
|
||||
} else {
|
||||
mSeries.setPrimaryRange(left, right);
|
||||
}
|
||||
}
|
||||
|
||||
public static class TimeAxis implements ChartAxis {
|
||||
private static final long TICK_INTERVAL = DateUtils.DAY_IN_MILLIS * 7;
|
||||
|
||||
|