diff options
author | Arne Schwabe <arne@rfc2549.org> | 2017-05-27 20:55:24 +0200 |
---|---|---|
committer | Arne Schwabe <arne@rfc2549.org> | 2017-05-27 20:55:24 +0200 |
commit | 715e6887d795a2e52290fd27e98b76c26b898830 (patch) | |
tree | 0509a3618b4ff2aa0d4463ffa88508f385a1de64 /main | |
parent | a45c9521e35dec3c64e71de2cc4006cf3d2be785 (diff) |
Implement graphs for traffic history
Diffstat (limited to 'main')
15 files changed, 729 insertions, 14 deletions
diff --git a/main/build.gradle b/main/build.gradle index dd68e170..f2b416bb 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -9,6 +9,7 @@ apply plugin: 'com.android.application' repositories { jcenter() + maven { url "https://jitpack.io" } } dependencies { @@ -16,13 +17,14 @@ dependencies { compile 'com.android.support:cardview-v7:25.3.1' compile 'com.android.support:recyclerview-v7:25.3.1' // compile 'ch.acra:acra:4.5.0' + compile 'com.github.PhilJay:MPAndroidChart:v3.0.2' testCompile 'junit:junit:4.12' } android { compileSdkVersion 25 - buildToolsVersion '25.0.2' + buildToolsVersion '25.0.3' defaultConfig { minSdkVersion 14 diff --git a/main/src/main/aidl/de/blinkt/openvpn/core/IServiceStatus.aidl b/main/src/main/aidl/de/blinkt/openvpn/core/IServiceStatus.aidl index 6254566a..5a5cbdb5 100644 --- a/main/src/main/aidl/de/blinkt/openvpn/core/IServiceStatus.aidl +++ b/main/src/main/aidl/de/blinkt/openvpn/core/IServiceStatus.aidl @@ -4,6 +4,8 @@ package de.blinkt.openvpn.core; // Declare any non-default types here with import statements import de.blinkt.openvpn.core.IStatusCallbacks; import android.os.ParcelFileDescriptor; +import de.blinkt.openvpn.core.TrafficHistory; + interface IServiceStatus { /** @@ -26,4 +28,9 @@ interface IServiceStatus { * Sets a cached password */ void setCachedPassword(in String uuid, int type, String password); + + /** + * Gets the traffic history + */ + TrafficHistory getTrafficHistory(); } diff --git a/main/src/main/aidl/de/blinkt/openvpn/core/IStatusCallbacks.aidl b/main/src/main/aidl/de/blinkt/openvpn/core/IStatusCallbacks.aidl index b72a2ffa..75860b81 100644 --- a/main/src/main/aidl/de/blinkt/openvpn/core/IStatusCallbacks.aidl +++ b/main/src/main/aidl/de/blinkt/openvpn/core/IStatusCallbacks.aidl @@ -9,6 +9,7 @@ import de.blinkt.openvpn.core.LogItem; import de.blinkt.openvpn.core.ConnectionStatus; + interface IStatusCallbacks { /** * Called when the service has a new status for you. diff --git a/main/src/main/aidl/de/blinkt/openvpn/core/TrafficHistory.aidl b/main/src/main/aidl/de/blinkt/openvpn/core/TrafficHistory.aidl new file mode 100644 index 00000000..5bd255fc --- /dev/null +++ b/main/src/main/aidl/de/blinkt/openvpn/core/TrafficHistory.aidl @@ -0,0 +1,4 @@ +package de.blinkt.openvpn.core; + + +parcelable TrafficHistory; diff --git a/main/src/main/java/de/blinkt/openvpn/activities/MainActivity.java b/main/src/main/java/de/blinkt/openvpn/activities/MainActivity.java index 8f1eca5b..43c816cc 100644 --- a/main/src/main/java/de/blinkt/openvpn/activities/MainActivity.java +++ b/main/src/main/java/de/blinkt/openvpn/activities/MainActivity.java @@ -23,6 +23,7 @@ import de.blinkt.openvpn.R; import de.blinkt.openvpn.fragments.AboutFragment; import de.blinkt.openvpn.fragments.FaqFragment; import de.blinkt.openvpn.fragments.GeneralSettings; +import de.blinkt.openvpn.fragments.GraphFragment; import de.blinkt.openvpn.fragments.LogFragment; import de.blinkt.openvpn.fragments.SendDumpFragment; import de.blinkt.openvpn.fragments.VPNProfileList; @@ -55,6 +56,7 @@ public class MainActivity extends BaseActivity { mPagerAdapter.addTab(R.string.vpn_list_title, VPNProfileList.class); + mPagerAdapter.addTab(R.string.graph, GraphFragment.class); mPagerAdapter.addTab(R.string.generalsettings, GeneralSettings.class); mPagerAdapter.addTab(R.string.faq, FaqFragment.class); @@ -63,6 +65,7 @@ public class MainActivity extends BaseActivity { mPagerAdapter.addTab(R.string.crashdump, SendDumpFragment.class); } + if (isDirectToTV()) mPagerAdapter.addTab(R.string.openvpn_log, LogFragment.class); diff --git a/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java b/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java index 2e68b202..a4c66e9d 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -122,7 +122,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac bytes = bytes * 8; int unit = mbit ? 1000 : 1024; if (bytes < unit) - return bytes + (mbit ? " bit" : " B"); + return bytes + (mbit ? "\u2009bit" : "\u2009B"); int exp = (int) (Math.log(bytes) / Math.log(unit)); String pre = (mbit ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (mbit ? "" : ""); diff --git a/main/src/main/java/de/blinkt/openvpn/core/OpenVPNStatusService.java b/main/src/main/java/de/blinkt/openvpn/core/OpenVPNStatusService.java index d025b2d4..6df1379a 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/OpenVPNStatusService.java +++ b/main/src/main/java/de/blinkt/openvpn/core/OpenVPNStatusService.java @@ -122,6 +122,11 @@ public class OpenVPNStatusService extends Service implements VpnStatus.LogListen PasswordCache.setCachedPassword(uuid, type, password); } + @Override + public TrafficHistory getTrafficHistory() throws RemoteException { + return VpnStatus.trafficHistory; + } + }; @Override diff --git a/main/src/main/java/de/blinkt/openvpn/core/StatusListener.java b/main/src/main/java/de/blinkt/openvpn/core/StatusListener.java index 16d580b7..5d0b7037 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/StatusListener.java +++ b/main/src/main/java/de/blinkt/openvpn/core/StatusListener.java @@ -36,6 +36,7 @@ public class StatusListener { if (service.queryLocalInterface("de.blinkt.openvpn.core.IServiceStatus") == null) { // Not a local service VpnStatus.setConnectedVPNProfile(serviceStatus.getLastConnectedVPN()); + VpnStatus.setTrafficHistory(serviceStatus.getTrafficHistory()); ParcelFileDescriptor pfd = serviceStatus.registerStatusCallback(mCallback); DataInputStream fd = new DataInputStream(new ParcelFileDescriptor.AutoCloseInputStream(pfd)); diff --git a/main/src/main/java/de/blinkt/openvpn/core/TrafficHistory.java b/main/src/main/java/de/blinkt/openvpn/core/TrafficHistory.java new file mode 100644 index 00000000..019f58be --- /dev/null +++ b/main/src/main/java/de/blinkt/openvpn/core/TrafficHistory.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2012-2017 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.core; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Vector; + +import static java.lang.Math.max; + +/** + * Created by arne on 23.05.17. + */ + +public class TrafficHistory implements Parcelable { + + private LinkedList<TrafficDatapoint> trafficHistorySeconds = new LinkedList<>(); + private LinkedList<TrafficDatapoint> trafficHistoryMinutes = new LinkedList<>(); + private LinkedList<TrafficDatapoint> trafficHistoryHours = new LinkedList<>(); + + private TrafficDatapoint lastSecondUsedForMinute; + private TrafficDatapoint lastMinuteUsedForHours; + + public TrafficHistory() { + + } + + protected TrafficHistory(Parcel in) { + in.readList(trafficHistorySeconds, getClass().getClassLoader()); + in.readList(trafficHistoryMinutes, getClass().getClassLoader()); + in.readList(trafficHistoryHours, getClass().getClassLoader()); + lastSecondUsedForMinute = in.readParcelable(getClass().getClassLoader()); + lastMinuteUsedForHours = in.readParcelable(getClass().getClassLoader()); + } + + public static final Creator<TrafficHistory> CREATOR = new Creator<TrafficHistory>() { + @Override + public TrafficHistory createFromParcel(Parcel in) { + return new TrafficHistory(in); + } + + @Override + public TrafficHistory[] newArray(int size) { + return new TrafficHistory[size]; + } + }; + + public LastDiff getLastDiff(TrafficDatapoint tdp) { + + TrafficDatapoint lasttdp; + + + if (trafficHistorySeconds.size() == 0) + lasttdp = new TrafficDatapoint(0, 0, System.currentTimeMillis()); + + else + lasttdp = trafficHistorySeconds.getLast(); + + if (tdp == null) { + tdp = lasttdp; + if (trafficHistorySeconds.size() < 2) + lasttdp = tdp; + else { + trafficHistorySeconds.descendingIterator().next(); + tdp = trafficHistorySeconds.descendingIterator().next(); + } + } + + return new LastDiff(lasttdp, tdp); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeList(trafficHistorySeconds); + dest.writeList(trafficHistoryMinutes); + dest.writeList(trafficHistoryHours); + dest.writeParcelable(lastSecondUsedForMinute, 0); + dest.writeParcelable(lastMinuteUsedForHours, 0); + + } + + public LinkedList<TrafficDatapoint> getHours() { + return trafficHistoryHours; + } + + public LinkedList<TrafficDatapoint> getMinutes() { + return trafficHistoryMinutes; + } + + public LinkedList<TrafficDatapoint> getSeconds() { + return trafficHistorySeconds; + } + + public static LinkedList<TrafficDatapoint> getDummyList() { + LinkedList<TrafficDatapoint> list = new LinkedList<>(); + list.add(new TrafficDatapoint(0, 0, System.currentTimeMillis())); + return list; + } + + + public static class TrafficDatapoint implements Parcelable { + private TrafficDatapoint(long inBytes, long outBytes, long timestamp) { + this.in = inBytes; + this.out = outBytes; + this.timestamp = timestamp; + } + + public final long timestamp; + public final long in; + public final long out; + + private TrafficDatapoint(Parcel in) { + timestamp = in.readLong(); + this.in = in.readLong(); + out = in.readLong(); + } + + public static final Creator<TrafficDatapoint> CREATOR = new Creator<TrafficDatapoint>() { + @Override + public TrafficDatapoint createFromParcel(Parcel in) { + return new TrafficDatapoint(in); + } + + @Override + public TrafficDatapoint[] newArray(int size) { + return new TrafficDatapoint[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(timestamp); + dest.writeLong(in); + dest.writeLong(out); + } + } + + LastDiff add(long in, long out) { + TrafficDatapoint tdp = new TrafficDatapoint(in, out, System.currentTimeMillis()); + + LastDiff diff = getLastDiff(tdp); + addDataPoint(tdp); + return diff; + } + + private void addDataPoint(TrafficDatapoint tdp) { + trafficHistorySeconds.add(tdp); + + if (lastSecondUsedForMinute == null) { + lastSecondUsedForMinute = new TrafficDatapoint(0, 0, 0); + lastMinuteUsedForHours = new TrafficDatapoint(0, 0, 0); + } + + removeAndAverage(tdp, true); + } + + private void removeAndAverage(TrafficDatapoint newTdp, boolean seconds) { + HashSet<TrafficDatapoint> toRemove = new HashSet<>(); + Vector<TrafficDatapoint> toAverage = new Vector<>(); + + long timePeriod; + LinkedList<TrafficDatapoint> tpList, nextList; + TrafficDatapoint lastTsPeriod; + + if (seconds) { + timePeriod = 60 * 1000; + tpList = trafficHistorySeconds; + nextList = trafficHistoryMinutes; + lastTsPeriod = lastSecondUsedForMinute; + } else { + timePeriod = 3600 * 1000; + tpList = trafficHistoryMinutes; + nextList = trafficHistoryHours; + lastTsPeriod = lastMinuteUsedForHours; + } + + if (newTdp.timestamp / timePeriod > (lastTsPeriod.timestamp / timePeriod)) { + nextList.add(newTdp); + + if (seconds) { + lastSecondUsedForMinute = newTdp; + removeAndAverage(newTdp, false); + } else + lastMinuteUsedForHours = newTdp; + + for (TrafficDatapoint tph : tpList) { + // List is iteratered from oldest to newest, remembert first one that we did not + if ((newTdp.timestamp - tph.timestamp) / timePeriod > 4) + toRemove.add(tph); + } + tpList.removeAll(toRemove); + } + } + + static class LastDiff { + + final private TrafficDatapoint tdp; + final private TrafficDatapoint lasttdp; + + private LastDiff(TrafficDatapoint lasttdp, TrafficDatapoint tdp) { + this.lasttdp = lasttdp; + this.tdp = tdp; + } + + public long getDiffOut() { + return max(0, tdp.out - lasttdp.out); + } + + public long getDiffIn() { + return max(0, tdp.in - lasttdp.in); + } + + public long getIn() { + return tdp.in; + } + + public long getOut() { + return tdp.out; + } + + } + + +}
\ No newline at end of file diff --git a/main/src/main/java/de/blinkt/openvpn/core/VpnStatus.java b/main/src/main/java/de/blinkt/openvpn/core/VpnStatus.java index c1f8a736..3adf7c88 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/VpnStatus.java +++ b/main/src/main/java/de/blinkt/openvpn/core/VpnStatus.java @@ -15,7 +15,9 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.LinkedList; import java.util.Locale; +import java.util.Queue; import java.util.Vector; +import java.util.concurrent.ConcurrentLinkedQueue; import de.blinkt.openvpn.R; import de.blinkt.openvpn.VpnProfile; @@ -35,13 +37,15 @@ public class VpnStatus { private static int mLastStateresid = R.string.state_noprocess; - private static long mlastByteCount[] = {0, 0, 0, 0}; private static HandlerThread mHandlerThread; private static String mLastConnectedVPNUUID; static boolean readFileLog =false; final static java.lang.Object readFileLock = new Object(); + + public static TrafficHistory trafficHistory; + public static void logException(LogLevel ll, String context, Exception e) { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); @@ -141,6 +145,10 @@ public class VpnStatus { return mLastConnectedVPNUUID; } + public static void setTrafficHistory(TrafficHistory trafficHistory) { + VpnStatus.trafficHistory = trafficHistory; + } + public enum LogLevel { INFO(2), @@ -194,7 +202,7 @@ public class VpnStatus { logListener = new Vector<>(); stateListener = new Vector<>(); byteCountListener = new Vector<>(); - + trafficHistory = new TrafficHistory(); logInformation(); @@ -248,7 +256,8 @@ public class VpnStatus { } public synchronized static void addByteCountListener(ByteCountListener bcl) { - bcl.updateByteCount(mlastByteCount[0], mlastByteCount[1], mlastByteCount[2], mlastByteCount[3]); + TrafficHistory.LastDiff diff = trafficHistory.getLastDiff(null); + bcl.updateByteCount(diff.getIn(), diff.getOut(), diff.getDiffIn(),diff.getDiffOut()); byteCountListener.add(bcl); } @@ -457,17 +466,10 @@ public class VpnStatus { public static synchronized void updateByteCount(long in, long out) { - long lastIn = mlastByteCount[0]; - long lastOut = mlastByteCount[1]; - long diffIn = mlastByteCount[2] = Math.max(0, in - lastIn); - long diffOut = mlastByteCount[3] = Math.max(0, out - lastOut); - + TrafficHistory.LastDiff diff = trafficHistory.add(in, out); - mlastByteCount = new long[]{in, out, diffIn, diffOut}; for (ByteCountListener bcl : byteCountListener) { - bcl.updateByteCount(in, out, diffIn, diffOut); + bcl.updateByteCount(in, out, diff.getDiffIn(), diff.getDiffOut()); } } - - } diff --git a/main/src/main/java/de/blinkt/openvpn/fragments/GraphFragment.java b/main/src/main/java/de/blinkt/openvpn/fragments/GraphFragment.java new file mode 100644 index 00000000..c3a63fa2 --- /dev/null +++ b/main/src/main/java/de/blinkt/openvpn/fragments/GraphFragment.java @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2012-2017 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ListView; +import android.widget.TextView; + +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.formatter.IAxisValueFormatter; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.core.OpenVPNManagement; +import de.blinkt.openvpn.core.TrafficHistory; +import de.blinkt.openvpn.core.VpnStatus; + +import static android.content.Context.MODE_PRIVATE; +import static de.blinkt.openvpn.core.OpenVPNService.humanReadableByteCount; +import static java.lang.Math.max; + +/** + * Created by arne on 19.05.17. + */ + +public class GraphFragment extends Fragment implements VpnStatus.ByteCountListener { + private static final String PREF_USE_LOG = "useLogGraph"; + private ListView mListView; + + private ChartDataAdapter mChartAdapter; + private int mColorIn; + private int mColorOut; + + private long firstTs; + private TextView mSpeedStatus; + private boolean mLogScale; + + private static final int TIME_PERIOD_SECDONS = 0; + private static final int TIME_PERIOD_MINUTES = 1; + private static final int TIME_PERIOD_HOURS = 2; + private Handler mHandler; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.graph, container, false); + mListView = (ListView) v.findViewById(R.id.graph_listview); + mSpeedStatus = (TextView) v.findViewById(R.id.speedStatus); + CheckBox logScaleView = (CheckBox) v.findViewById(R.id.useLogScale); + mLogScale = getActivity().getPreferences(MODE_PRIVATE).getBoolean(PREF_USE_LOG, false); + logScaleView.setChecked(mLogScale); + + List<Integer> charts = new LinkedList<>(); + charts.add(TIME_PERIOD_SECDONS); + charts.add(TIME_PERIOD_MINUTES); + charts.add(TIME_PERIOD_HOURS); + + mChartAdapter = new ChartDataAdapter(getActivity(), charts); + mListView.setAdapter(mChartAdapter); + + mColorIn = getActivity().getResources().getColor(R.color.dataIn); + mColorOut = getActivity().getResources().getColor(R.color.dataOut); + + + logScaleView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mLogScale = isChecked; + mChartAdapter.notifyDataSetChanged(); + getActivity().getPreferences(MODE_PRIVATE).edit().putBoolean(PREF_USE_LOG, isChecked).apply(); + } + }); + + mHandler = new Handler(); + + return v; + + + } + + private Runnable triggerRefresh = new Runnable() { + @Override + public void run() { + mChartAdapter.notifyDataSetChanged(); + mHandler.postDelayed(triggerRefresh, OpenVPNManagement.mBytecountInterval*1500); + } + }; + + @Override + public void onResume() { + super.onResume(); + + VpnStatus.addByteCountListener(this); + mHandler.postDelayed(triggerRefresh, OpenVPNManagement.mBytecountInterval*1500); + } + + @Override + public void onPause() { + super.onPause(); + + mHandler.removeCallbacks(triggerRefresh); + VpnStatus.removeByteCountListener(this); + } + + @Override + public void updateByteCount(long in, long out, long diffIn, long diffOut) { + if (firstTs == 0) + firstTs = System.currentTimeMillis() / 100; + + long now = (System.currentTimeMillis() / 100) - firstTs; + int interval = OpenVPNManagement.mBytecountInterval * 10; + + final String netstat = String.format(getString(R.string.statusline_bytecount), + humanReadableByteCount(in, false), + humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true), + humanReadableByteCount(out, false), + humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true)); + + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mHandler.removeCallbacks(triggerRefresh); + mSpeedStatus.setText(netstat); + mChartAdapter.notifyDataSetChanged(); + mHandler.postDelayed(triggerRefresh, OpenVPNManagement.mBytecountInterval*1500); + } + }); + + } + + private class ChartDataAdapter extends ArrayAdapter<Integer> { + + private Context mContext; + + public ChartDataAdapter(Context context, List<Integer> trafficData) { + super(context, 0, trafficData); + mContext = context; + } + + @NonNull + @Override + public View getView(final int position, @Nullable View convertView, @NonNull ViewGroup parent) { + ViewHolder holder = null; + + if (convertView == null) { + + holder = new ViewHolder(); + + convertView = LayoutInflater.from(mContext).inflate( + R.layout.graph_item, parent, false); + holder.chart = (LineChart) convertView.findViewById(R.id.chart); + holder.title = (TextView) convertView.findViewById(R.id.tvName); + convertView.setTag(holder); + + } else { + holder = (ViewHolder) convertView.getTag(); + } + + // apply styling + // holder.chart.setValueTypeface(mTf); + holder.chart.getDescription().setEnabled(false); + holder.chart.setDrawGridBackground(false); + + XAxis xAxis = holder.chart.getXAxis(); + xAxis.setPosition(XAxis.XAxisPosition.BOTTOM); + xAxis.setDrawGridLines(false); + xAxis.setDrawAxisLine(true); + + switch (position){ + case TIME_PERIOD_HOURS: + holder.title.setText(R.string.avghour); + break; + case TIME_PERIOD_MINUTES: + holder.title.setText(R.string.avgmin); + break; + default: + holder.title.setText(R.string.last5minutes); + break; + } + + xAxis.setValueFormatter(new IAxisValueFormatter() { + + + @Override + public String getFormattedValue(float value, AxisBase axis) { + switch (position){ + case TIME_PERIOD_HOURS: + return String.format(Locale.getDefault(), "%.0f\u2009h ago", (axis.getAxisMaximum() - value) / 10/3600); + case TIME_PERIOD_MINUTES: + return String.format(Locale.getDefault(), "%.0f\u2009m ago", (axis.getAxisMaximum() - value) / 10/60); + default: + return String.format(Locale.getDefault(), "%.0f\u2009s ago", (axis.getAxisMaximum() - value) / 10); + } + + } + }); + xAxis.setLabelCount(5); + + YAxis yAxis = holder.chart.getAxisLeft(); + yAxis.setLabelCount(5, false); + + yAxis.setValueFormatter(new IAxisValueFormatter() { + @Override + public String getFormattedValue(float value, AxisBase axis) { + if (mLogScale && value < 2.1f) + return "< 100\u2009bit"; + if (mLogScale) + value = (float) Math.pow(10, value)/8; + + return humanReadableByteCount((long) value, true); + } + }); + + holder.chart.getAxisRight().setEnabled(false); + + LineData data = getDataSet(position); + float ymax = data.getYMax(); + + if (mLogScale) { + yAxis.setAxisMinimum(2f); + yAxis.setAxisMaximum((float) Math.ceil(ymax)); + yAxis.setLabelCount((int) (Math.ceil(ymax -2f))); + } else { + yAxis.setAxisMinimum(0f); + yAxis.resetAxisMaximum(); + yAxis.setLabelCount(6); + } + + if (data.getDataSetByIndex(0).getEntryCount() < 3) + holder.chart.setData(null); + else + holder.chart.setData(data); + + holder.chart.setNoDataText(getString(R.string.notenoughdata)); + + holder.chart.invalidate(); + //holder.chart.animateX(750); + + return convertView; + } + + + public void getAverageForGraphList(boolean in, int timeperiod) { + + + + } + + private LineData getDataSet(int timeperiod) { + + LinkedList<Entry> dataIn = new LinkedList<>(); + LinkedList<Entry> dataOut = new LinkedList<>(); + + long interval; + + LinkedList<TrafficHistory.TrafficDatapoint> list; + switch (timeperiod) { + case TIME_PERIOD_HOURS: + list = VpnStatus.trafficHistory.getHours(); + interval = 3600 ; + break; + case TIME_PERIOD_MINUTES: + list = VpnStatus.trafficHistory.getMinutes(); + interval = 60; + break; + default: + list = VpnStatus.trafficHistory.getSeconds(); + interval = OpenVPNManagement.mBytecountInterval ; + break; + } + if (list.size()==0) { + list = TrafficHistory.getDummyList(); + } + + long firstTimestamp = list.peek().timestamp; + long lastBytecountIn = list.peek().in; + long lastBytecountOut = list.peek().out; + + long lastts=0; + + for (TrafficHistory.TrafficDatapoint tdp: list){ + float t = (tdp.timestamp - firstTimestamp) / 100f; + + float in = (tdp.in - lastBytecountIn)/ (float) interval; + float out = (tdp.out - lastBytecountOut) / (float) interval; + + lastBytecountIn = tdp.in; + lastBytecountOut = tdp.out; + + if (mLogScale) { + in = max(2f, (float) Math.log10(in*8)); + out = max(2f, (float) Math.log10(out* 8)); + } + + if (lastts > 0 && ( tdp.timestamp -lastts> 2 * interval*1000)){ + dataIn.add(new Entry((lastts- firstTimestamp+ interval)/100f, 0)); + dataOut.add(new Entry((lastts- firstTimestamp+ interval)/100f, 0)); + + dataIn.add(new Entry(t - interval/100f, 0)); + dataOut.add(new Entry(t - interval/100f, 0)); + } + + lastts = tdp.timestamp; + + dataIn.add(new Entry(t,in)); + dataOut.add(new Entry(t,out)); + + } + long now = System.currentTimeMillis(); + if (list.peekLast().timestamp < now-interval*1000l) { + dataIn.add(new Entry((now-firstTimestamp)/100, 0)); + dataOut.add(new Entry((now-firstTimestamp) /100, 0)); + + if (now -lastts > 2 * interval*1000) { + dataIn.add(new Entry((lastts- firstTimestamp+ interval)/100f, 0)); + dataOut.add(new Entry((lastts- firstTimestamp+ interval)/100f, 0)); + } + } + + ArrayList<ILineDataSet> dataSets = new ArrayList<>(); + + + LineDataSet indata = new LineDataSet(dataIn, "In"); + LineDataSet outdata = new LineDataSet(dataOut, "Out"); + + setLineDataAttributes(indata, mColorIn); + setLineDataAttributes(outdata, mColorOut); + + dataSets.add(indata); + dataSets.add(outdata); + + return new LineData(dataSets); + } + + private void setLineDataAttributes(LineDataSet dataSet, int colour) { + dataSet.setLineWidth(2); + dataSet.setCircleRadius(1); + dataSet.setDrawCircles(false); + dataSet.setDrawFilled(true); + dataSet.setFillAlpha(42); + dataSet.setFillColor(colour); + dataSet.setColor(colour); + dataSet.setMode(LineDataSet.Mode.LINEAR); + + dataSet.setDrawValues(false); + } + } + + private static class ViewHolder { + LineChart chart; + TextView title; + } +} diff --git a/main/src/main/res/layout/graph.xml b/main/src/main/res/layout/graph.xml new file mode 100644 index 00000000..d734623e --- /dev/null +++ b/main/src/main/res/layout/graph.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (c) 2012-2017 Arne Schwabe + ~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + --> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + + <TextView + android:id="@+id/speedStatus" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + tools:text="some speed status" /> + + <CheckBox + android:id="@+id/useLogScale" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_above="@id/speedStatus" + android:text="@string/use_logarithmic_scale" /> + + <ListView + android:id="@+id/graph_listview" + android:layout_above="@id/useLogScale" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + +</RelativeLayout>
\ No newline at end of file diff --git a/main/src/main/res/layout/graph_item.xml b/main/src/main/res/layout/graph_item.xml new file mode 100644 index 00000000..da90342b --- /dev/null +++ b/main/src/main/res/layout/graph_item.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (c) 2012-2017 Arne Schwabe + ~ Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + --> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools" + android:padding="8dp"> + + <TextView + android:id="@+id/tvName" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:layout_marginLeft="4dp" + android:layout_marginStart="4dp" + tools:text="Medium Text" + android:textSize="16sp" /> + + <com.github.mikephil.charting.charts.LineChart + android:layout_below="@id/tvName" + android:id="@+id/chart" + android:layout_width="match_parent" + android:layout_height="200dp" /> + +</RelativeLayout>
\ No newline at end of file diff --git a/main/src/main/res/values/colours.xml b/main/src/main/res/values/colours.xml index d06bc233..cf4a2a7f 100644 --- a/main/src/main/res/values/colours.xml +++ b/main/src/main/res/values/colours.xml @@ -18,4 +18,7 @@ <color name="background_tab_pressed">#1AFFFFFF</color> + + <color name="dataIn">#ff0000</color> + <color name="dataOut">#0000ff</color> </resources>
\ No newline at end of file diff --git a/main/src/main/res/values/strings.xml b/main/src/main/res/values/strings.xml index 69ddadd4..2077d02e 100755 --- a/main/src/main/res/values/strings.xml +++ b/main/src/main/res/values/strings.xml @@ -424,5 +424,11 @@ <string name="sorted_az">Profiles sorted by name</string> <string name="deprecated_tls_remote">Config uses option tls-remote that was deprecated in 2.3 and finally removed in 2.4</string> <string name="auth_failed_behaviour">Behaviour on AUTH_FAILED</string> + <string name="graph">Graph</string> + <string name="use_logarithmic_scale">Use logarithmic scale</string> + <string name="notenoughdata">Not enough data</string> + <string name="avghour">Average per hour</string> + <string name="avgmin">Average per minute</string> + <string name="last5minutes">Last 5 minutes</string> </resources> |