QS: Basic cellular data detail panel.

Change-Id: I1f19a8bf3e01d7f1d49e82d7096a215c863eab61
This commit is contained in:
John Spurlock
2014-07-08 17:09:42 -04:00
parent 857025678d
commit b98f747c71
11 changed files with 482 additions and 5 deletions

View File

@@ -53,7 +53,7 @@
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.MANAGE_NETWORK_POLICY" />
<uses-permission android:name="android.permission.CONNECTIVITY_INTERNAL" />
<uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />
<!-- Physical hardware -->
<uses-permission android:name="android.permission.MANAGE_USB" />

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2014 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.QS.DataUsage" />
<TextView
android:id="@+id/usage_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.QS.DataUsage.Usage" />
<com.android.systemui.qs.DataUsageGraph
android:id="@+id/usage_graph"
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_marginBottom="@dimen/qs_panel_padding"
android:layout_marginTop="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:id="@+id/usage_carrier_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="@style/TextAppearance.QS.DataUsage" />
<TextView
android:id="@+id/usage_info_top_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="@style/TextAppearance.QS.DataUsage" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/usage_period_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="@style/TextAppearance.QS.DataUsage.Secondary" />
<TextView
android:id="@+id/usage_info_bottom_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="@style/TextAppearance.QS.DataUsage.Secondary" />
</LinearLayout>
</LinearLayout>

View File

@@ -36,12 +36,14 @@
<color name="qs_batterymeter_frame_color">#FF404040</color>
<color name="system_primary_color">#ff263238</color>
<color name="system_secondary_color">#ff384248</color>
<color name="system_accent_color">#ff7fcac3</color>
<color name="system_accent_color">#ff80CBC4</color><!-- deep teal 200 -->
<color name="system_error_color">#fff0592b</color>
<color name="qs_tile_divider">#29ffffff</color><!-- 16% white -->
<color name="qs_tile_text">#B3FFFFFF</color><!-- 70% white -->
<color name="qs_subhead">#66FFFFFF</color><!-- 40% white -->
<color name="status_bar_clock_color">#FFFFFFFF</color>
<color name="data_usage_secondary">#99FFFFFF</color><!-- 60% white -->
<color name="data_usage_graph_track">#33FFFFFF</color><!-- 20% white -->
<color name="status_bar_clock_color">#33FFFFFF</color>
<!-- Tint color for the content on the notification overflow card. -->
<color name="keyguard_overflow_content_color">#ff686868</color>

View File

@@ -546,6 +546,10 @@
<string name="quick_settings_notifications_label">Notifications</string>
<!-- QuickSettings: Flashlight [CHAR LIMIT=NONE] -->
<string name="quick_settings_flashlight_label">Flashlight</string>
<!-- QuickSettings: Cellular detail panel title [CHAR LIMIT=NONE] -->
<string name="quick_settings_cellular_detail_title">Cellular data</string>
<!-- QuickSettings: Cellular detail panel, data usage title [CHAR LIMIT=NONE] -->
<string name="quick_settings_cellular_detail_data_usage">Data usage</string>
<!-- Recents: The empty recents string. [CHAR LIMIT=NONE] -->
<string name="recents_empty_message">No recent apps</string>

View File

@@ -169,7 +169,7 @@
<style name="TextAppearance.QS.DetailItemSecondary">
<item name="android:textSize">14sp</item>
<item name="android:textColor">#7fcac3</item>
<item name="android:textColor">@color/system_accent_color</item>
</style>
<style name="TextAppearance.QS.DetailButton">
@@ -196,6 +196,19 @@
<item name="android:fontFamily">sans-serif-medium</item>
</style>
<style name="TextAppearance.QS.DataUsage">
<item name="android:textSize">14sp</item>
</style>
<style name="TextAppearance.QS.DataUsage.Usage">
<item name="android:textSize">36sp</item>
<item name="android:textColor">@color/system_accent_color</item>
</style>
<style name="TextAppearance.QS.DataUsage.Secondary">
<item name="android:textColor">@color/data_usage_secondary</item>
</style>
<style name="BaseBrightnessDialogContainer">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2014 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.systemui.qs;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import com.android.systemui.R;
public class DataUsageGraph extends View {
private final int mBackgroundColor;
private final int mUsageColor;
private final RectF mTmpRect = new RectF();
private final Paint mTmpPaint = new Paint();
private long mMaxLevel = 1;
private long mLimitLevel;
private long mWarningLevel;
private long mUsageLevel;
public DataUsageGraph(Context context, AttributeSet attrs) {
super(context, attrs);
mBackgroundColor = context.getResources().getColor(R.color.data_usage_graph_track);
mUsageColor = context.getResources().getColor(R.color.system_accent_color);
}
public void setLevels(long maxLevel, long limitLevel, long warningLevel, long usageLevel) {
mMaxLevel = Math.max(maxLevel, 1);
mLimitLevel = limitLevel;
mWarningLevel = warningLevel;
mUsageLevel = usageLevel;
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final RectF r = mTmpRect;
final Paint p = mTmpPaint;
final int w = getWidth();
final int h = getHeight();
// draw background
r.set(0, 0, w, h);
p.setColor(mBackgroundColor);
canvas.drawRect(r, p);
// draw usage
r.set(0, 0, w * mUsageLevel / (float) mMaxLevel, h);
p.setColor(mUsageColor);
canvas.drawRect(r, p);
}
}

View File

@@ -20,24 +20,34 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.android.systemui.R;
import com.android.systemui.qs.DataUsageGraph;
import com.android.systemui.qs.QSTile;
import com.android.systemui.qs.QSTileView;
import com.android.systemui.qs.SignalTileView;
import com.android.systemui.statusbar.policy.NetworkController;
import com.android.systemui.statusbar.policy.NetworkController.DataUsageInfo;
import com.android.systemui.statusbar.policy.NetworkController.NetworkSignalChangedCallback;
import java.text.DecimalFormat;
/** Quick settings tile: Cellular **/
public class CellularTile extends QSTile<QSTile.SignalState> {
private static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName(
"com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity"));
private final NetworkController mController;
private final CellularDetailAdapter mDetailAdapter;
public CellularTile(Host host) {
super(host);
mController = host.getNetworkController();
mDetailAdapter = new CellularDetailAdapter();
}
@Override
@@ -45,6 +55,11 @@ public class CellularTile extends QSTile<QSTile.SignalState> {
return new SignalState();
}
@Override
public DetailAdapter getDetailAdapter() {
return mDetailAdapter;
}
@Override
public void setListening(boolean listening) {
if (listening) {
@@ -61,7 +76,11 @@ public class CellularTile extends QSTile<QSTile.SignalState> {
@Override
protected void handleClick() {
mHost.startSettingsActivity(CELLULAR_SETTINGS);
if (mController.isMobileDataSupported()) {
showDetail(true);
} else {
mHost.startSettingsActivity(CELLULAR_SETTINGS);
}
}
@Override
@@ -157,5 +176,81 @@ public class CellularTile extends QSTile<QSTile.SignalState> {
public void onAirplaneModeChanged(boolean enabled) {
// noop
}
public void onMobileDataEnabled(boolean enabled) {
mDetailAdapter.setMobileDataEnabled(enabled);
}
};
private final class CellularDetailAdapter implements DetailAdapter {
private static final double KB = 1024;
private static final double MB = 1024 * KB;
private static final double GB = 1024 * MB;
private final DecimalFormat FORMAT = new DecimalFormat("#.##");
@Override
public int getTitle() {
return R.string.quick_settings_cellular_detail_title;
}
@Override
public Boolean getToggleState() {
return mController.isMobileDataSupported() ? mController.isMobileDataEnabled() : null;
}
@Override
public Intent getSettingsIntent() {
return CELLULAR_SETTINGS;
}
@Override
public void setToggleState(boolean state) {
mController.setMobileDataEnabled(state);
}
@Override
public View createDetailView(Context context, View convertView, ViewGroup parent) {
final View v = convertView != null ? convertView : LayoutInflater.from(mContext)
.inflate(R.layout.data_usage, parent, false);
final DataUsageInfo info = mController.getDataUsageInfo();
if (info == null) return v;
final TextView title = (TextView) v.findViewById(android.R.id.title);
title.setText(R.string.quick_settings_cellular_detail_data_usage);
final TextView usage = (TextView) v.findViewById(R.id.usage_text);
usage.setText(formatBytes(info.usageLevel));
final DataUsageGraph graph = (DataUsageGraph) v.findViewById(R.id.usage_graph);
graph.setLevels(info.maxLevel, info.limitLevel, info.warningLevel, info.usageLevel);
final TextView carrier = (TextView) v.findViewById(R.id.usage_carrier_text);
carrier.setText(info.carrier);
final TextView period = (TextView) v.findViewById(R.id.usage_period_text);
period.setText(info.period);
final TextView infoTop = (TextView) v.findViewById(R.id.usage_info_top_text);
// TODO
final TextView infoBottom = (TextView) v.findViewById(R.id.usage_info_bottom_text);
// TODO
return v;
}
public void setMobileDataEnabled(boolean enabled) {
fireToggleStateChanged(enabled);
}
private String formatBytes(long bytes) {
final long b = Math.abs(bytes);
double val;
String suffix;
if (b > 100 * MB) {
val = b / GB;
suffix = "GB";
} else if (b > 100 * KB) {
val = b / MB;
suffix = "MB";
} else {
val = b / KB;
suffix = "KB";
}
return FORMAT.format(val * (bytes < 0 ? -1 : 1)) + " " + suffix;
}
}
}

View File

@@ -192,6 +192,11 @@ public class WifiTile extends QSTile<QSTile.SignalState> {
public void onAirplaneModeChanged(boolean enabled) {
// noop
}
@Override
public void onMobileDataEnabled(boolean enabled) {
// noop
}
};
private final class WifiDetailAdapter implements DetailAdapter,

View File

@@ -0,0 +1,153 @@
/*
* Copyright (C) 2014 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.systemui.statusbar.policy;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.NetworkStatsHistory.FIELD_RX_BYTES;
import static android.net.NetworkStatsHistory.FIELD_TX_BYTES;
import static android.telephony.TelephonyManager.SIM_STATE_READY;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.INetworkStatsService;
import android.net.INetworkStatsSession;
import android.net.NetworkStatsHistory;
import android.net.NetworkTemplate;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.telephony.TelephonyManager;
import android.text.format.DateUtils;
import android.util.Log;
import com.android.systemui.statusbar.policy.NetworkController.DataUsageInfo;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MobileDataController {
private static final String TAG = "MobileDataController";
private static final boolean DEBUG = true;
private static final SimpleDateFormat MMM_D = new SimpleDateFormat("MMM d");
private static final int FIELDS = FIELD_RX_BYTES | FIELD_TX_BYTES;
private final Context mContext;
private final TelephonyManager mTelephonyManager;
private final ConnectivityManager mConnectivityManager;
private final INetworkStatsService mStatsService;
private INetworkStatsSession mSession;
private Callback mCallback;
public MobileDataController(Context context) {
mContext = context;
mTelephonyManager = TelephonyManager.from(context);
mConnectivityManager = ConnectivityManager.from(context);
mStatsService = INetworkStatsService.Stub.asInterface(
ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
try {
mSession = mStatsService.openSession();
} catch (RemoteException e) {
Log.w(TAG, "Failed to open stats session");
mSession = null;
}
}
public void setCallback(Callback callback) {
mCallback = callback;
}
private DataUsageInfo warn(String msg) {
Log.w(TAG, "Failed to get data usage, " + msg);
return null;
}
public DataUsageInfo getDataUsageInfo() {
final String subscriberId = getActiveSubscriberId(mContext);
if (subscriberId == null) {
return warn("no subscriber id");
}
if (mSession == null) {
return warn("no stats session");
}
final NetworkTemplate template = NetworkTemplate.buildTemplateMobileAll(subscriberId);
try {
final NetworkStatsHistory history = mSession.getHistoryForNetwork(template, FIELDS);
final long now = System.currentTimeMillis();
// period = last 4 wks for now
final long start = now - DateUtils.WEEK_IN_MILLIS * 4;
final long end = now;
final long callStart = System.currentTimeMillis();
final NetworkStatsHistory.Entry entry = history.getValues(start, end, now, null);
final long callEnd = System.currentTimeMillis();
if (DEBUG) Log.d(TAG, String.format("history call from %s to %s now=%s took %sms: %s",
new Date(start), new Date(end), new Date(now), callEnd - callStart,
historyEntryToString(entry)));
if (entry == null) {
return warn("no entry data");
}
final long totalBytes = entry.rxBytes + entry.txBytes;
final DataUsageInfo usage = new DataUsageInfo();
usage.maxLevel = (long) (totalBytes / .4);
usage.usageLevel = totalBytes;
usage.period = MMM_D.format(new Date(start)) + " - " + MMM_D.format(new Date(end));
return usage;
} catch (RemoteException e) {
return warn("remote call failed");
}
}
private static String historyEntryToString(NetworkStatsHistory.Entry entry) {
return entry == null ? null : new StringBuilder("Entry[")
.append("bucketDuration=").append(entry.bucketDuration)
.append(",bucketStart=").append(entry.bucketStart)
.append(",activeTime=").append(entry.activeTime)
.append(",rxBytes=").append(entry.rxBytes)
.append(",rxPackets=").append(entry.rxPackets)
.append(",txBytes=").append(entry.txBytes)
.append(",txPackets=").append(entry.txPackets)
.append(",operations=").append(entry.operations)
.append(']').toString();
}
public void setMobileDataEnabled(boolean enabled) {
mTelephonyManager.setDataEnabled(enabled);
if (mCallback != null) {
mCallback.onMobileDataEnabled(enabled);
}
}
public boolean isMobileDataSupported() {
// require both supported network and ready SIM
return mConnectivityManager.isNetworkSupported(TYPE_MOBILE)
&& mTelephonyManager.getSimState() == SIM_STATE_READY;
}
public boolean isMobileDataEnabled() {
return mTelephonyManager.getDataEnabled();
}
private static String getActiveSubscriberId(Context context) {
final TelephonyManager tele = TelephonyManager.from(context);
final String actualSubscriberId = tele.getSubscriberId();
return actualSubscriberId;
}
public interface Callback {
void onMobileDataEnabled(boolean enabled);
}
}

View File

@@ -32,12 +32,17 @@ public interface NetworkController {
boolean activityIn, boolean activityOut,
String dataTypeContentDescriptionId, String description, boolean noSim);
void onAirplaneModeChanged(boolean enabled);
void onMobileDataEnabled(boolean enabled);
}
void addAccessPointCallback(AccessPointCallback callback);
void removeAccessPointCallback(AccessPointCallback callback);
void scanForAccessPoints();
void connect(AccessPoint ap);
boolean isMobileDataSupported();
boolean isMobileDataEnabled();
void setMobileDataEnabled(boolean enabled);
DataUsageInfo getDataUsageInfo();
public interface AccessPointCallback {
void onAccessPointsChanged(AccessPoint[] accessPoints);
@@ -52,4 +57,13 @@ public interface NetworkController {
public boolean isConnected;
public int level; // 0 - 5
}
public static class DataUsageInfo {
public String carrier;
public String period;
public long maxLevel;
public long limitLevel;
public long warningLevel;
public long usageLevel;
}
}

View File

@@ -170,6 +170,7 @@ public class NetworkControllerImpl extends BroadcastReceiver
}
private final WifiAccessPointController mAccessPoints;
private final MobileDataController mMobileDataController;
/**
* Construct this controller object and register for updates.
@@ -240,6 +241,19 @@ public class NetworkControllerImpl extends BroadcastReceiver
mLastLocale = mContext.getResources().getConfiguration().locale;
mAccessPoints = new WifiAccessPointController(mContext);
mMobileDataController = new MobileDataController(mContext);
mMobileDataController.setCallback(new MobileDataController.Callback() {
@Override
public void onMobileDataEnabled(boolean enabled) {
notifyMobileDataEnabled(enabled);
}
});
}
private void notifyMobileDataEnabled(boolean enabled) {
for (NetworkSignalChangedCallback cb : mSignalsChangedCallbacks) {
cb.onMobileDataEnabled(enabled);
}
}
public boolean hasMobileDataFeature() {
@@ -322,6 +336,28 @@ public class NetworkControllerImpl extends BroadcastReceiver
}.execute();
}
@Override
public DataUsageInfo getDataUsageInfo() {
final DataUsageInfo info = mMobileDataController.getDataUsageInfo();
info.carrier = mNetworkName;
return info;
}
@Override
public boolean isMobileDataSupported() {
return mMobileDataController.isMobileDataSupported();
}
@Override
public boolean isMobileDataEnabled() {
return mMobileDataController.isMobileDataEnabled();
}
@Override
public void setMobileDataEnabled(boolean enabled) {
mMobileDataController.setMobileDataEnabled(enabled);
}
public void refreshSignalCluster(SignalCluster cluster) {
if (mDemoMode) return;
cluster.setWifiIndicators(