17 | * In order to compare qualities use the {@link .compareTo()} method. Qualities are ordered in increasing
18 | * order of declaration as per the java docs for {@link java.lang.Enum}.
19 | *
20 | */
21 | public enum ConnectionQuality {
22 | /**
23 | * Bandwidth under 150 kbps.
24 | */
25 | POOR,
26 | /**
27 | * Bandwidth between 150 and 550 kbps.
28 | */
29 | MODERATE,
30 | /**
31 | * Bandwidth between 550 and 2000 kbps.
32 | */
33 | GOOD,
34 | /**
35 | * EXCELLENT - Bandwidth over 2000 kbps.
36 | */
37 | EXCELLENT,
38 | /**
39 | * Placeholder for unknown bandwidth. This is the initial value and will stay at this value
40 | * if a bandwidth cannot be accurately found.
41 | */
42 | UNKNOWN
43 | }
44 |
--------------------------------------------------------------------------------
/connectionclass-sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 |
14 |
20 |
21 |
26 |
27 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD License
2 |
3 | For Network Connection Class software
4 |
5 | Copyright (c) 2015, Facebook, Inc. All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without modification,
8 | are permitted provided that the following conditions are met:
9 |
10 | * Redistributions of source code must retain the above copyright notice, this
11 | list of conditions and the following disclaimer.
12 |
13 | * Redistributions in binary form must reproduce the above copyright notice,
14 | this list of conditions and the following disclaimer in the documentation
15 | and/or other materials provided with the distribution.
16 |
17 | * Neither the name Facebook nor the names of its contributors may be used to
18 | endorse or promote products derived from this software without specific
19 | prior written permission.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/connectionclass/src/main/java/com/facebook/network/connectionclass/ExponentialGeometricAverage.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | *
9 | */
10 |
11 | package com.facebook.network.connectionclass;
12 |
13 | /**
14 | * Moving average calculation for ConnectionClass.
15 | */
16 | class ExponentialGeometricAverage {
17 |
18 | private final double mDecayConstant;
19 | private final int mCutover;
20 |
21 | private double mValue = -1;
22 | private int mCount;
23 |
24 | public ExponentialGeometricAverage(double decayConstant) {
25 | mDecayConstant = decayConstant;
26 | mCutover = decayConstant == 0.0
27 | ? Integer.MAX_VALUE
28 | : (int) Math.ceil(1 / decayConstant);
29 | }
30 |
31 | /**
32 | * Adds a new measurement to the moving average.
33 | * @param measurement - Bandwidth measurement in bits/ms to add to the moving average.
34 | */
35 | public void addMeasurement(double measurement) {
36 | double keepConstant = 1 - mDecayConstant;
37 | if (mCount > mCutover) {
38 | mValue = Math.exp(keepConstant * Math.log(mValue) + mDecayConstant * Math.log(measurement));
39 | } else if (mCount > 0) {
40 | double retained = keepConstant * mCount / (mCount + 1.0);
41 | double newcomer = 1.0 - retained;
42 | mValue = Math.exp(retained * Math.log(mValue) + newcomer * Math.log(measurement));
43 | } else {
44 | mValue = measurement;
45 | }
46 | mCount++;
47 | }
48 |
49 | public double getAverage() {
50 | return mValue;
51 | }
52 |
53 | /**
54 | * Reset the moving average.
55 | */
56 | public void reset() {
57 | mValue = -1.0;
58 | mCount = 0;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Network Connection Class
2 | We want to make contributing to this project as easy and transparent as
3 | possible.
4 |
5 | ## Our Development Process
6 | We work directly in the github project and provide versioned releases
7 | appropriate for major milestones and minor bug fixes or improvements. GitHub
8 | is used directly for issues and pull requests and the developers actively
9 | respond to requests.
10 |
11 | ## Pull Requests
12 | We actively welcome your pull requests.
13 | 1. Fork the repo and create your branch from `master`.
14 | 2. If you've added code that should be tested, add tests
15 | 3. If you've changed APIs, update the documentation.
16 | 4. Ensure the test suite passes.
17 | 5. Make sure your code lints.
18 | 6. If you haven't already, complete the Contributor License Agreement ("CLA").
19 |
20 | ## Contributor License Agreement ("CLA")
21 | In order to accept your pull request, we need you to submit a CLA. You only need
22 | to do this once to work on any of Facebook's open source projects.
23 |
24 | Complete your CLA here:
25 |
26 | ## Issues
27 | We use GitHub issues to track public bugs. Please ensure your description is
28 | clear and has sufficient instructions to be able to reproduce the issue.
29 |
30 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe
31 | disclosure of security bugs. In those cases, please go through the process
32 | outlined on that page and do not file a public issue.
33 |
34 | ## Coding Style
35 | * 2 spaces for indentation rather than tabs
36 | * Line wrapping indents 4 spaces
37 | * 100 character line length
38 | * One parameter per line when line wrapping is required
39 | * Use the `m` member variable prefix for private fields
40 | * Opening braces to appear on the same line as code
41 |
42 | ## License
43 | By contributing to Network Connection Class, you agree that your contributions will be licensed
44 | under its BSD license.
--------------------------------------------------------------------------------
/PATENTS:
--------------------------------------------------------------------------------
1 | Additional Grant of Patent Rights Version 2
2 |
3 | "Software" means the Network Connection Class software distributed by Facebook, Inc.
4 |
5 | Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software
6 | ("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable
7 | (subject to the termination provision below) license under any Necessary
8 | Claims, to make, have made, use, sell, offer to sell, import, and otherwise
9 | transfer the Software. For avoidance of doubt, no license is granted under
10 | Facebook’s rights in any patent claims that are infringed by (i) modifications
11 | to the Software made by you or any third party or (ii) the Software in
12 | combination with any software or other technology.
13 |
14 | The license granted hereunder will terminate, automatically and without notice,
15 | if you (or any of your subsidiaries, corporate affiliates or agents) initiate
16 | directly or indirectly, or take a direct financial interest in, any Patent
17 | Assertion: (i) against Facebook or any of its subsidiaries or corporate
18 | affiliates, (ii) against any party if such Patent Assertion arises in whole or
19 | in part from any software, technology, product or service of Facebook or any of
20 | its subsidiaries or corporate affiliates, or (iii) against any party relating
21 | to the Software. Notwithstanding the foregoing, if Facebook or any of its
22 | subsidiaries or corporate affiliates files a lawsuit alleging patent
23 | infringement against you in the first instance, and you respond by filing a
24 | patent infringement counterclaim in that lawsuit against that party that is
25 | unrelated to the Software, the license granted hereunder will not terminate
26 | under section (i) of this paragraph due to such counterclaim.
27 |
28 | A "Necessary Claim" is a claim of a patent owned by Facebook that is
29 | necessarily infringed by the Software standing alone.
30 |
31 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect,
32 | or contributory infringement or inducement to infringe any patent, including a
33 | cross-claim or counterclaim.
34 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #  Network Connection Class
2 |
3 | Network Connection Class is an Android library that allows you to figure out
4 | the quality of the current user's internet connection. The connection gets
5 | classified into several "Connection Classes" that make it easy to develop
6 | against. The library does this by listening to the existing internet traffic
7 | done by your app and notifying you when the user's connection quality changes.
8 | Developers can then use this Connection Class information and adjust the application's
9 | behaviour (request lower quality images or video, throttle type-ahead, etc).
10 |
11 | Network Connection Class currently only measures the user's downstream bandwidth.
12 | Latency is also an important factor, but in our tests, we've found that bandwidth
13 | is a good proxy for both.
14 |
15 | The Network Connection Class library takes care of spikes using a moving average
16 | of the incoming samples, and also applies some hysteresis (both with a minimum
17 | number of samples and amount the average has to cross a boundary before triggering
18 | a bucket change):
19 | 
20 |
21 | ## Integration
22 |
23 | ### Download
24 | Download [the latest JARs](https://github.com/facebook/network-connection-class/releases/latest) or grab via Gradle:
25 | ```groovy
26 | compile 'com.facebook.network.connectionclass:connectionclass:1.0.1'
27 | ```
28 | or Maven:
29 | ```xml
30 |
31 | com.facebook.network.connectionclass
32 | connectionclass
33 | 1.0.1
34 |
35 | ```
36 |
37 | ### Calculate Connection Class
38 | Connection Class provides an interface for classes to add themselves as
39 | listeners for when the network's connection quality changes. In the subscriber
40 | class, implement `ConnectionClassStateChangeListener`:
41 |
42 | ```java
43 | public interface ConnectionClassStateChangeListener {
44 | public void onBandwidthStateChange(ConnectionQuality bandwidthState);
45 | }
46 | ```
47 |
48 | and subscribe with the listener:
49 |
50 | ```java
51 | ConnectionClassManager.getInstance().register(mListener);
52 | ```
53 |
54 | Alternatively, you can manually query for the current connection quality bucket with
55 | `getCurrentBandwidthQuality()`.
56 |
57 | ```java
58 | ConnectionQuality cq = ConnectionClassManager.getInstance().getCurrentBandwidthQuality();
59 | ```
60 |
61 | The main way to provide the ConnectionClassManager data is to use the DeviceBandwidthSampler.
62 | The DeviceBandwidthSampler samples the device's underlying network stats when you tell it
63 | you're performing some sort of network activity (downloading photos, playing a video, etc).
64 |
65 | ```java
66 | // Override ConnectionClassStateChangeListener
67 | ConnectionClassManager.getInstance().register(mListener);
68 | DeviceBandwidthSampler.getInstance().startSampling();
69 | // Do some downloading tasks
70 | DeviceBandwidthSampler.getInstance().stopSampling();
71 | ```
72 |
73 | If the application is aware of the bandwidth downloaded in a certain time frame,
74 | data can be added to the moving average using:
75 |
76 | ```java
77 | ConnectionClassManager.addBandwidth(bandwidth, time);
78 | ```
79 |
80 | See the `connectionclass-sample` project for more details.
81 |
82 | ## Improve Connection Class!
83 | See the [CONTRIBUTING.md](https://github.com/facebook/network-connection-class/blob/master/CONTRIBUTING.md) file for how to help out.
84 |
85 | ## License
86 | Connection Class is BSD-licensed. We also provide an additional patent grant.
87 |
--------------------------------------------------------------------------------
/release.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven'
2 | apply plugin: 'signing'
3 |
4 | def isReleaseBuild() {
5 | return VERSION_NAME.contains("SNAPSHOT") == false
6 | }
7 |
8 | def getRepositoryUrl() {
9 | return hasProperty('repositoryUrl') ? property('repositoryUrl') : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
10 | }
11 |
12 | def getRepositoryUsername() {
13 | return hasProperty('repositoryUsername') ? property('repositoryUsername') : ""
14 | }
15 |
16 | def getRepositoryPassword() {
17 | return hasProperty('repositoryPassword') ? property('repositoryPassword') : ""
18 | }
19 |
20 | afterEvaluate { project ->
21 | task androidJavadoc(type: Javadoc) {
22 | source = android.sourceSets.main.java.srcDirs
23 | classpath += files(android.bootClasspath)
24 | }
25 |
26 | task androidJavadocJar(type: Jar) {
27 | classifier = 'javadoc'
28 | from androidJavadoc.destinationDir
29 | }
30 |
31 | task androidSourcesJar(type: Jar) {
32 | classifier = 'sources'
33 | from android.sourceSets.main.java.srcDirs
34 | }
35 |
36 | android.libraryVariants.all { variant ->
37 | def name = variant.name.capitalize()
38 | task "jar${name}"(type: Jar, dependsOn: variant.javaCompile) {
39 | from variant.javaCompile.destinationDir
40 | }
41 | }
42 |
43 | artifacts {
44 | archives androidJavadocJar
45 | archives androidSourcesJar
46 | archives jarRelease
47 | }
48 |
49 | version = VERSION_NAME
50 | group = GROUP
51 |
52 | signing {
53 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
54 | sign configurations.archives
55 | }
56 |
57 | uploadArchives {
58 | configuration = configurations.archives
59 | repositories.mavenDeployer {
60 | beforeDeployment {
61 | MavenDeployment deployment -> signing.signPom(deployment)
62 | }
63 |
64 | repository(url: getRepositoryUrl()) {
65 | authentication(
66 | userName: getRepositoryUsername(),
67 | password: getRepositoryPassword())
68 |
69 | }
70 |
71 | pom.project {
72 | name POM_NAME
73 | artifactId POM_ARTIFACT_ID
74 | packaging POM_PACKAGING
75 | description 'Android Network Connection Class Library'
76 | url 'https://github.com/facebook/network-connection-class'
77 |
78 | scm {
79 | url 'https://github.com/facebook/network-connection-class.git'
80 | connection 'scm:git:https://github.com/facebook/network-connection-class.git'
81 | developerConnection 'scm:git:git@github.com:facebook/network-connection-class.git'
82 | }
83 |
84 | licenses {
85 | license {
86 | name 'BSD License'
87 | url 'https://github.com/facebook/network-connection-class/blob/master/LICENSE'
88 | distribution 'repo'
89 | }
90 | }
91 |
92 | developers {
93 | developer {
94 | id 'facebook'
95 | name 'Facebook'
96 | }
97 | }
98 | }
99 | }
100 | }
101 |
102 | task installArchives(type: Upload) {
103 | configuration = configurations.archives
104 | repositories {
105 | mavenDeployer {
106 | repository url: "file://${System.properties['user.home']}/.m2/repository"
107 | }
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/connectionclass/src/main/java/com/facebook/network/connectionclass/ByteArrayScanner.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | *
9 | */
10 |
11 | package com.facebook.network.connectionclass;
12 |
13 | import javax.annotation.Nullable;
14 | import java.util.NoSuchElementException;
15 |
16 | class ByteArrayScanner {
17 | private @Nullable byte[] mData;
18 | private int mCurrentOffset;
19 | private int mTotalLength;
20 | private char mDelimiter;
21 | private boolean mDelimiterSet;
22 |
23 | public ByteArrayScanner reset(byte[] buffer, int length) {
24 | mData = buffer;
25 | mCurrentOffset = 0;
26 | mTotalLength = length;
27 | mDelimiterSet = false;
28 | return this;
29 | }
30 |
31 | public ByteArrayScanner useDelimiter(char delimiter) {
32 | throwIfNotReset();
33 | mDelimiter = delimiter;
34 | mDelimiterSet = true;
35 | return this;
36 | }
37 |
38 | private void throwIfNotReset() {
39 | if (mData == null) {
40 | throw new IllegalStateException("Must call reset first");
41 | }
42 | }
43 |
44 | private void throwIfDelimiterNotSet() {
45 | if (!mDelimiterSet) {
46 | throw new IllegalStateException("Must call useDelimiter first");
47 | }
48 | }
49 |
50 | /**
51 | * @return The next token, parsed as a string.
52 | * @throws NoSuchElementException
53 | */
54 | public String nextString()
55 | throws NoSuchElementException {
56 | throwIfNotReset();
57 | throwIfDelimiterNotSet();
58 | int offset = mCurrentOffset;
59 | int length = advance();
60 | return new String(mData, offset, length);
61 | }
62 |
63 | /**
64 | * Matches the next token with a string.
65 | * @param str String to match the next token with.
66 | * @return True if the next token matches, false otherwise.
67 | * @throws NoSuchElementException
68 | */
69 | public boolean nextStringEquals(String str)
70 | throws NoSuchElementException {
71 | int offset = mCurrentOffset;
72 | int length = advance();
73 | if (str.length() != length) {
74 | return false;
75 | }
76 | for (int i = 0; i < str.length(); i++) {
77 | if (str.charAt(i) != mData[offset]) {
78 | return false;
79 | }
80 | offset++;
81 | }
82 | return true;
83 | }
84 |
85 | /**
86 | * @return The next token, parsed as an integer.
87 | * @throws NoSuchElementException
88 | */
89 | public int nextInt()
90 | throws NoSuchElementException{
91 | throwIfNotReset();
92 | throwIfDelimiterNotSet();
93 | int offset = mCurrentOffset;
94 | int length = advance();
95 | int value = parseInt(
96 | mData,
97 | offset,
98 | offset + length);
99 | return value;
100 | }
101 |
102 | /**
103 | * Move to the next token.
104 | * @throws NoSuchElementException
105 | */
106 | public void skip()
107 | throws NoSuchElementException {
108 | throwIfNotReset();
109 | throwIfDelimiterNotSet();
110 | advance();
111 | }
112 |
113 | private int advance()
114 | throws NoSuchElementException {
115 | throwIfNotReset();
116 | throwIfDelimiterNotSet();
117 | if (mTotalLength <= mCurrentOffset) {
118 | throw new NoSuchElementException("Reading past end of input stream at " + mCurrentOffset + ".");
119 | }
120 | int index = indexOf(
121 | mData,
122 | mCurrentOffset,
123 | mTotalLength,
124 | mDelimiter);
125 | if (index == -1) {
126 | int length = mTotalLength - mCurrentOffset;
127 | mCurrentOffset = mTotalLength;
128 | return length;
129 | } else {
130 | int length = index - mCurrentOffset;
131 | mCurrentOffset = index + 1;
132 | return length;
133 | }
134 | }
135 |
136 | private static int parseInt(byte[] buffer, int start, int end)
137 | throws NumberFormatException {
138 | int radix = 10;
139 | int result = 0;
140 | while (start < end) {
141 | int digit = buffer[start++] - '0';
142 | if (digit < 0 || digit > 9) {
143 | throw new NumberFormatException("Invalid int in buffer at " + (start - 1) + ".");
144 | }
145 | int next = result * radix + digit;
146 | result = next;
147 | }
148 | return result;
149 | }
150 |
151 | private static int indexOf(byte[] data, int start, int end, char ch) {
152 | for (int i = start; i < end; i++) {
153 | if (data[i] == ch) {
154 | return i;
155 | }
156 | }
157 | return -1;
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/connectionclass-sample/src/main/java/com/facebook/network/connectionclass/sample/MainActivity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | *
9 | */
10 |
11 | package com.facebook.network.connectionclass.sample;
12 |
13 | import android.app.Activity;
14 | import android.os.AsyncTask;
15 | import android.os.Bundle;
16 |
17 | import android.util.Log;
18 | import android.view.View;
19 | import android.widget.TextView;
20 | import com.facebook.network.connectionclass.*;
21 |
22 | import java.io.IOException;
23 | import java.io.InputStream;
24 | import java.net.URL;
25 | import java.net.URLConnection;
26 |
27 |
28 | public class MainActivity extends Activity {
29 |
30 | private static final String TAG = "ConnectionClass-Sample";
31 |
32 | private ConnectionClassManager mConnectionClassManager;
33 | private DeviceBandwidthSampler mDeviceBandwidthSampler;
34 | private ConnectionChangedListener mListener;
35 | private TextView mTextView;
36 | private View mRunningBar;
37 |
38 | private String mURL = "http://connectionclass.parseapp.com/m100_hubble_4060.jpg";
39 | private int mTries = 0;
40 | private ConnectionQuality mConnectionClass = ConnectionQuality.UNKNOWN;
41 |
42 | @Override
43 | protected void onCreate(Bundle savedInstanceState) {
44 | super.onCreate(savedInstanceState);
45 | setContentView(R.layout.activity_main);
46 | mConnectionClassManager = ConnectionClassManager.getInstance();
47 | mDeviceBandwidthSampler = DeviceBandwidthSampler.getInstance();
48 | findViewById(R.id.test_btn).setOnClickListener(testButtonClicked);
49 | mTextView = (TextView)findViewById(R.id.connection_class);
50 | mTextView.setText(mConnectionClassManager.getCurrentBandwidthQuality().toString());
51 | mRunningBar = findViewById(R.id.runningBar);
52 | mRunningBar.setVisibility(View.GONE);
53 | mListener = new ConnectionChangedListener();
54 | }
55 |
56 | @Override
57 | protected void onPause() {
58 | super.onPause();
59 | mConnectionClassManager.remove(mListener);
60 | }
61 |
62 | @Override
63 | protected void onResume() {
64 | super.onResume();
65 | mConnectionClassManager.register(mListener);
66 | }
67 |
68 | /**
69 | * Listener to update the UI upon connectionclass change.
70 | */
71 | private class ConnectionChangedListener
72 | implements ConnectionClassManager.ConnectionClassStateChangeListener {
73 |
74 | @Override
75 | public void onBandwidthStateChange(ConnectionQuality bandwidthState) {
76 | mConnectionClass = bandwidthState;
77 | runOnUiThread(new Runnable() {
78 | @Override
79 | public void run() {
80 | mTextView.setText(mConnectionClass.toString());
81 | }
82 | });
83 | }
84 | }
85 |
86 | private final View.OnClickListener testButtonClicked = new View.OnClickListener() {
87 | @Override
88 | public void onClick(View v) {
89 | new DownloadImage().execute(mURL);
90 | }
91 | };
92 |
93 | /**
94 | * AsyncTask for handling downloading and making calls to the timer.
95 | */
96 | private class DownloadImage extends AsyncTask {
97 |
98 | @Override
99 | protected void onPreExecute() {
100 | mDeviceBandwidthSampler.startSampling();
101 | mRunningBar.setVisibility(View.VISIBLE);
102 | }
103 |
104 | @Override
105 | protected Void doInBackground(String... url) {
106 | String imageURL = url[0];
107 | try {
108 | // Open a stream to download the image from our URL.
109 | URLConnection connection = new URL(imageURL).openConnection();
110 | connection.setUseCaches(false);
111 | connection.connect();
112 | InputStream input = connection.getInputStream();
113 | try {
114 | byte[] buffer = new byte[1024];
115 |
116 | // Do some busy waiting while the stream is open.
117 | while (input.read(buffer) != -1) {
118 | }
119 | } finally {
120 | input.close();
121 | }
122 | } catch (IOException e) {
123 | Log.e(TAG, "Error while downloading image.");
124 | }
125 | return null;
126 | }
127 |
128 | @Override
129 | protected void onPostExecute(Void v) {
130 | mDeviceBandwidthSampler.stopSampling();
131 | // Retry for up to 10 times until we find a ConnectionClass.
132 | if (mConnectionClass == ConnectionQuality.UNKNOWN && mTries < 10) {
133 | mTries++;
134 | new DownloadImage().execute(mURL);
135 | }
136 | if (!mDeviceBandwidthSampler.isSampling()) {
137 | mRunningBar.setVisibility(View.GONE);
138 | }
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/connectionclass/src/main/java/com/facebook/network/connectionclass/DeviceBandwidthSampler.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | *
9 | */
10 |
11 | package com.facebook.network.connectionclass;
12 |
13 | import android.net.TrafficStats;
14 | import android.os.Handler;
15 | import android.os.HandlerThread;
16 | import android.os.Looper;
17 | import android.os.Message;
18 | import android.os.SystemClock;
19 |
20 | import java.util.concurrent.atomic.AtomicInteger;
21 |
22 | import javax.annotation.Nonnull;
23 |
24 | /**
25 | * Class used to read from TrafficStats periodically, in order to determine a ConnectionClass.
26 | */
27 | public class DeviceBandwidthSampler {
28 |
29 | /**
30 | * The DownloadBandwidthManager that keeps track of the moving average and ConnectionClass.
31 | */
32 | private final ConnectionClassManager mConnectionClassManager;
33 |
34 | private AtomicInteger mSamplingCounter;
35 |
36 | private SamplingHandler mHandler;
37 | private HandlerThread mThread;
38 |
39 | private long mLastTimeReading;
40 | private static long sPreviousBytes = -1;
41 |
42 | // Singleton.
43 | private static class DeviceBandwidthSamplerHolder {
44 | public static final DeviceBandwidthSampler instance =
45 | new DeviceBandwidthSampler(ConnectionClassManager.getInstance());
46 | }
47 |
48 | /**
49 | * Retrieval method for the DeviceBandwidthSampler singleton.
50 | * @return The singleton instance of DeviceBandwidthSampler.
51 | */
52 | @Nonnull
53 | public static DeviceBandwidthSampler getInstance() {
54 | return DeviceBandwidthSamplerHolder.instance;
55 | }
56 |
57 | private DeviceBandwidthSampler(
58 | ConnectionClassManager connectionClassManager) {
59 | mConnectionClassManager = connectionClassManager;
60 | mSamplingCounter = new AtomicInteger();
61 | mThread = new HandlerThread("ParseThread");
62 | mThread.start();
63 | mHandler = new SamplingHandler(mThread.getLooper());
64 | }
65 |
66 | /**
67 | * Method call to start sampling for download bandwidth.
68 | */
69 | public void startSampling() {
70 | if (mSamplingCounter.getAndIncrement() == 0) {
71 | mHandler.startSamplingThread();
72 | mLastTimeReading = SystemClock.elapsedRealtime();
73 | }
74 | }
75 |
76 | /**
77 | * Finish sampling and prevent further changes to the
78 | * ConnectionClass until another timer is started.
79 | */
80 | public void stopSampling() {
81 | if (mSamplingCounter.decrementAndGet() == 0) {
82 | mHandler.stopSamplingThread();
83 | addFinalSample();
84 | }
85 | }
86 |
87 | /**
88 | * Method for polling for the change in total bytes since last update and
89 | * adding it to the BandwidthManager.
90 | */
91 | protected void addSample() {
92 | long newBytes = TrafficStats.getTotalRxBytes();
93 | long byteDiff = newBytes - sPreviousBytes;
94 | if (sPreviousBytes >= 0) {
95 | synchronized (this) {
96 | long curTimeReading = SystemClock.elapsedRealtime();
97 | mConnectionClassManager.addBandwidth(byteDiff, curTimeReading - mLastTimeReading);
98 |
99 | mLastTimeReading = curTimeReading;
100 | }
101 | }
102 | sPreviousBytes = newBytes;
103 | }
104 |
105 | /**
106 | * Resets previously read byte count after recording a sample, so that
107 | * we don't count bytes downloaded in between sampling sessions.
108 | */
109 | protected void addFinalSample() {
110 | addSample();
111 | sPreviousBytes = -1;
112 | }
113 |
114 | /**
115 | * @return True if there are still threads which are sampling, false otherwise.
116 | */
117 | public boolean isSampling() {
118 | return (mSamplingCounter.get() != 0);
119 | }
120 |
121 | private class SamplingHandler extends Handler {
122 | /**
123 | * Time between polls in ms.
124 | */
125 | static final long SAMPLE_TIME = 1000;
126 |
127 | static private final int MSG_START = 1;
128 |
129 | public SamplingHandler(Looper looper) {
130 | super(looper);
131 | }
132 |
133 | @Override
134 | public void handleMessage(Message msg) {
135 | switch (msg.what) {
136 | case MSG_START:
137 | addSample();
138 | sendEmptyMessageDelayed(MSG_START, SAMPLE_TIME);
139 | break;
140 | default:
141 | throw new IllegalArgumentException("Unknown what=" + msg.what);
142 | }
143 | }
144 |
145 |
146 | public void startSamplingThread() {
147 | sendEmptyMessage(SamplingHandler.MSG_START);
148 | }
149 |
150 | public void stopSamplingThread() {
151 | removeMessages(SamplingHandler.MSG_START);
152 | }
153 | }
154 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/connectionclass/src/test/java/com/facebook/network/connectionclass/ConnectionClassTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | *
9 | */
10 |
11 | package com.facebook.network.connectionclass;
12 |
13 | import org.junit.Before;
14 | import org.junit.Test;
15 | import org.junit.runner.RunWith;
16 | import org.mockito.Mock;
17 | import org.powermock.modules.junit4.PowerMockRunner;
18 |
19 | import static org.junit.Assert.assertEquals;
20 |
21 | @RunWith(PowerMockRunner.class)
22 | public class ConnectionClassTest {
23 |
24 | @Mock
25 | public ConnectionClassManager mConnectionClassManager;
26 | public TestBandwidthStateChangeListener mTestBandwidthStateChangeListener;
27 |
28 | private static final long BYTES_TO_BITS = 8;
29 |
30 | @Before
31 | public void setUp() {
32 | mConnectionClassManager = ConnectionClassManager.getInstance();
33 | mTestBandwidthStateChangeListener = new TestBandwidthStateChangeListener();
34 | mConnectionClassManager.reset();
35 | }
36 |
37 | //Test the moving average to make sure correct results are returned.
38 | @Test
39 | public void TestMovingAverage() {
40 | mConnectionClassManager.addBandwidth(620000L, 1000L);
41 | mConnectionClassManager.addBandwidth(630000L, 1000L);
42 | mConnectionClassManager.addBandwidth(670000L, 1000L);
43 | mConnectionClassManager.addBandwidth(500000L, 1000L);
44 | mConnectionClassManager.addBandwidth(550000L, 1000L);
45 | mConnectionClassManager.addBandwidth(590000L, 1000L);
46 | assertEquals(ConnectionQuality.EXCELLENT, mConnectionClassManager.getCurrentBandwidthQuality());
47 | }
48 |
49 | //Test that values under the lower bandwidth bound do not affect the final ConnectionClass values.
50 | @Test
51 | public void TestGarbageValues() {
52 | mConnectionClassManager.addBandwidth(620000L, 1000L);
53 | mConnectionClassManager.addBandwidth(0L, 1000L);
54 | mConnectionClassManager.addBandwidth(630000L, 1000L);
55 | mConnectionClassManager.addBandwidth(5L, 1000L);
56 | mConnectionClassManager.addBandwidth(10L, 1000L);
57 | mConnectionClassManager.addBandwidth(0L, 1000L);
58 | mConnectionClassManager.addBandwidth(90L, 1000L);
59 | mConnectionClassManager.addBandwidth(200L, 1000L);
60 | mConnectionClassManager.addBandwidth(670000L, 1000L);
61 | mConnectionClassManager.addBandwidth(500000L, 1000L);
62 | mConnectionClassManager.addBandwidth(550000L, 1000L);
63 | mConnectionClassManager.addBandwidth(590000L, 1000L);
64 | assertEquals(ConnectionQuality.EXCELLENT, mConnectionClassManager.getCurrentBandwidthQuality());
65 | }
66 |
67 | @Test
68 | public void testStateChangeBroadcastNoBroadcast() {
69 | for (int i = 0; i < ConnectionClassManager.DEFAULT_SAMPLES_TO_QUALITY_CHANGE - 1; i++) {
70 | mConnectionClassManager.addBandwidth(1000, 2);
71 | }
72 | assertEquals(0, mTestBandwidthStateChangeListener.getNumberOfStateChanges());
73 | }
74 |
75 | @Test
76 | public void testStateChangeBroadcastWithBroadcast() {
77 | mConnectionClassManager.reset();
78 | for (int i = 0; i < ConnectionClassManager.DEFAULT_SAMPLES_TO_QUALITY_CHANGE + 1; i++) {
79 | mConnectionClassManager.addBandwidth(1000, 2);
80 | }
81 | assertEquals(1, mTestBandwidthStateChangeListener.getNumberOfStateChanges());
82 | assertEquals(ConnectionQuality.EXCELLENT, mTestBandwidthStateChangeListener.getLastBandwidthState());
83 | }
84 |
85 | @Test
86 | public void testStateChangeBroadcastDoesNotRepeatItself() {
87 | mConnectionClassManager.reset();
88 | for (int i = 0; i < 3 * ConnectionClassManager.DEFAULT_SAMPLES_TO_QUALITY_CHANGE + 1; i++) {
89 | mConnectionClassManager.addBandwidth(1000, 2);
90 | }
91 | assertEquals(1, mTestBandwidthStateChangeListener.getNumberOfStateChanges());
92 | }
93 |
94 | @Test
95 | public void testStateChangeHysteresisRejectsLow() {
96 | runHysteresisTest(
97 | ConnectionClassManager.DEFAULT_POOR_BANDWIDTH,
98 | 1.02,
99 | ConnectionQuality.MODERATE,
100 | (100.0 - ConnectionClassManager.DEFAULT_HYSTERESIS_PERCENT / 2) / 100.0,
101 | ConnectionQuality.MODERATE);
102 | }
103 |
104 | @Test
105 | public void testStateChangeHysteresisRejectsHigh() {
106 | runHysteresisTest(
107 | ConnectionClassManager.DEFAULT_MODERATE_BANDWIDTH,
108 | .98,
109 | ConnectionQuality.MODERATE,
110 | 100.0 / (100.0 - ConnectionClassManager.DEFAULT_HYSTERESIS_PERCENT / 2),
111 | ConnectionQuality.MODERATE);
112 | }
113 |
114 | @Test
115 | public void testStateChangeHysteresisAcceptsLow() {
116 | runHysteresisTest(
117 | ConnectionClassManager.DEFAULT_POOR_BANDWIDTH,
118 | 1.02,
119 | ConnectionQuality.MODERATE,
120 | (100.0 - ConnectionClassManager.DEFAULT_HYSTERESIS_PERCENT * 2) / 100.0,
121 | ConnectionQuality.POOR);
122 | }
123 |
124 | @Test
125 | public void testStateChangeHysteresisAcceptsHigh() {
126 | runHysteresisTest(
127 | ConnectionClassManager.DEFAULT_MODERATE_BANDWIDTH,
128 | 0.98,
129 | ConnectionQuality.MODERATE,
130 | 100.0 / (100.0 - ConnectionClassManager.DEFAULT_HYSTERESIS_PERCENT * 2),
131 | ConnectionQuality.GOOD);
132 | }
133 |
134 | private void runHysteresisTest(
135 | double bandwidthBoundary,
136 | double initialMultiplier,
137 | ConnectionQuality initialQuality,
138 | double finalMultiplier,
139 | ConnectionQuality finalQuality) {
140 | int milliseconds = 5;
141 |
142 | // Run just enough samples to set the initial quality.
143 | for (int i = 0; i < ConnectionClassManager.DEFAULT_SAMPLES_TO_QUALITY_CHANGE + 1; i++) {
144 | long barelyModerateBytes = bytesPerUpdate(bandwidthBoundary, initialMultiplier, milliseconds);
145 | mConnectionClassManager.addBandwidth(barelyModerateBytes, milliseconds);
146 | }
147 | assertEquals(initialQuality, mTestBandwidthStateChangeListener.getLastBandwidthState());
148 |
149 | // Run enough samples at the new rate that the moving average should now be close to this rate.
150 | for (int i = 0; i < 2 * ConnectionClassManager.DEFAULT_SAMPLES_TO_QUALITY_CHANGE; i++) {
151 | long quitePoorBytes = bytesPerUpdate(bandwidthBoundary, finalMultiplier, milliseconds);;
152 | mConnectionClassManager.addBandwidth(quitePoorBytes, milliseconds);
153 | }
154 | assertEquals(finalQuality, mTestBandwidthStateChangeListener.getLastBandwidthState());
155 | }
156 |
157 | static private long bytesPerUpdate(
158 | double bandwidthBoundary,
159 | double multiplier,
160 | long milliseconds) {
161 | double bytes = bandwidthBoundary * multiplier * milliseconds / BYTES_TO_BITS;
162 | bytes = multiplier > 1.0 ? Math.ceil(bytes) : Math.floor(bytes);
163 | return (long) bytes;
164 | }
165 |
166 | private class TestBandwidthStateChangeListener implements
167 | ConnectionClassManager.ConnectionClassStateChangeListener {
168 |
169 | private int mNumberOfStateChanges = 0;
170 | private ConnectionQuality mLastBandwidthState;
171 |
172 | private TestBandwidthStateChangeListener() {
173 | mConnectionClassManager.register(this);
174 | }
175 |
176 | @Override
177 | public void onBandwidthStateChange(ConnectionQuality bandwidthState) {
178 | mNumberOfStateChanges += 1;
179 | mLastBandwidthState = bandwidthState;
180 | }
181 |
182 | public int getNumberOfStateChanges() {
183 | return mNumberOfStateChanges;
184 | }
185 |
186 | public ConnectionQuality getLastBandwidthState() {
187 | return mLastBandwidthState;
188 | }
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/connectionclass/src/main/java/com/facebook/network/connectionclass/ConnectionClassManager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | *
9 | */
10 |
11 | package com.facebook.network.connectionclass;
12 |
13 | import javax.annotation.Nonnull;
14 | import java.util.ArrayList;
15 | import java.util.concurrent.atomic.AtomicReference;
16 |
17 | /**
18 | *
19 | * Class used to calculate the approximate bandwidth of a user's connection.
20 | *
21 | *
22 | * This class notifies all subscribed {@link ConnectionClassStateChangeListener} with the new
23 | * ConnectionClass when the network's ConnectionClass changes.
24 | *
25 | */
26 | public class ConnectionClassManager {
27 |
28 | /*package*/ static final double DEFAULT_SAMPLES_TO_QUALITY_CHANGE = 5;
29 | private static final int BYTES_TO_BITS = 8;
30 |
31 | /**
32 | * Default values for determining quality of data connection.
33 | * Bandwidth numbers are in Kilobits per second (kbps).
34 | */
35 | /*package*/ static final int DEFAULT_POOR_BANDWIDTH = 150;
36 | /*package*/ static final int DEFAULT_MODERATE_BANDWIDTH = 550;
37 | /*package*/ static final int DEFAULT_GOOD_BANDWIDTH = 2000;
38 | /*package*/ static final long DEFAULT_HYSTERESIS_PERCENT = 20;
39 | private static final double HYSTERESIS_TOP_MULTIPLIER = 100.0 / (100.0 - DEFAULT_HYSTERESIS_PERCENT);
40 | private static final double HYSTERESIS_BOTTOM_MULTIPLIER = (100.0 - DEFAULT_HYSTERESIS_PERCENT) / 100.0;
41 |
42 | /**
43 | * The factor used to calculate the current bandwidth
44 | * depending upon the previous calculated value for bandwidth.
45 | *
46 | * The smaller this value is, the less responsive to new samples the moving average becomes.
47 | */
48 | private static final double DEFAULT_DECAY_CONSTANT = 0.05;
49 |
50 | /** Current bandwidth of the user's connection depending upon the response. */
51 | private ExponentialGeometricAverage mDownloadBandwidth
52 | = new ExponentialGeometricAverage(DEFAULT_DECAY_CONSTANT);
53 | private volatile boolean mInitiateStateChange = false;
54 | private AtomicReference mCurrentBandwidthConnectionQuality =
55 | new AtomicReference(ConnectionQuality.UNKNOWN);
56 | private AtomicReference mNextBandwidthConnectionQuality;
57 | private ArrayList mListenerList =
58 | new ArrayList();
59 | private int mSampleCounter;
60 |
61 | /**
62 | * The lower bound for measured bandwidth in bits/ms. Readings
63 | * lower than this are treated as effectively zero (therefore ignored).
64 | */
65 | static final long BANDWIDTH_LOWER_BOUND = 10;
66 |
67 | // Singleton.
68 | private static class ConnectionClassManagerHolder {
69 | public static final ConnectionClassManager instance = new ConnectionClassManager();
70 | }
71 |
72 | /**
73 | * Retrieval method for the DownloadBandwidthManager singleton.
74 | * @return The singleton instance of DownloadBandwidthManager.
75 | */
76 | @Nonnull
77 | public static ConnectionClassManager getInstance() {
78 | return ConnectionClassManagerHolder.instance;
79 | }
80 |
81 | // Force constructor to be private.
82 | private ConnectionClassManager() {}
83 |
84 | /**
85 | * Adds bandwidth to the current filtered latency counter. Sends a broadcast to all
86 | * {@link ConnectionClassStateChangeListener} if the counter moves from one bucket
87 | * to another (i.e. poor bandwidth -> moderate bandwidth).
88 | */
89 | public synchronized void addBandwidth(long bytes, long timeInMs) {
90 |
91 | //Ignore garbage values.
92 | if (timeInMs == 0 || (bytes) * 1.0 / (timeInMs) * BYTES_TO_BITS < BANDWIDTH_LOWER_BOUND) {
93 | return;
94 | }
95 |
96 | double bandwidth = (bytes) * 1.0 / (timeInMs) * BYTES_TO_BITS;
97 | mDownloadBandwidth.addMeasurement(bandwidth);
98 |
99 | if (mInitiateStateChange) {
100 | mSampleCounter += 1;
101 | if (getCurrentBandwidthQuality() != mNextBandwidthConnectionQuality.get()) {
102 | mInitiateStateChange = false;
103 | mSampleCounter = 1;
104 | }
105 | if (mSampleCounter >= DEFAULT_SAMPLES_TO_QUALITY_CHANGE && significantlyOutsideCurrentBand()) {
106 | mInitiateStateChange = false;
107 | mSampleCounter = 1;
108 | mCurrentBandwidthConnectionQuality.set(mNextBandwidthConnectionQuality.get());
109 | notifyListeners();
110 | }
111 | return;
112 | }
113 |
114 | if (mCurrentBandwidthConnectionQuality.get() != getCurrentBandwidthQuality()) {
115 | mInitiateStateChange = true;
116 | mNextBandwidthConnectionQuality =
117 | new AtomicReference(getCurrentBandwidthQuality());
118 | }
119 | }
120 |
121 | private boolean significantlyOutsideCurrentBand() {
122 | if (mDownloadBandwidth == null) {
123 | // Make Infer happy. It wouldn't make any sense to call this while mDownloadBandwidth is null.
124 | return false;
125 | }
126 | ConnectionQuality currentQuality = mCurrentBandwidthConnectionQuality.get();
127 | double bottomOfBand;
128 | double topOfBand;
129 | switch (currentQuality) {
130 | case POOR:
131 | bottomOfBand = 0;
132 | topOfBand = DEFAULT_POOR_BANDWIDTH;
133 | break;
134 | case MODERATE:
135 | bottomOfBand = DEFAULT_POOR_BANDWIDTH;
136 | topOfBand = DEFAULT_MODERATE_BANDWIDTH;
137 | break;
138 | case GOOD:
139 | bottomOfBand = DEFAULT_MODERATE_BANDWIDTH;
140 | topOfBand = DEFAULT_GOOD_BANDWIDTH;
141 | break;
142 | case EXCELLENT:
143 | bottomOfBand = DEFAULT_GOOD_BANDWIDTH;
144 | topOfBand = Float.MAX_VALUE;
145 | break;
146 | default: // If current quality is UNKNOWN, then changing is always valid.
147 | return true;
148 | }
149 | double average = mDownloadBandwidth.getAverage();
150 | if (average > topOfBand) {
151 | if (average > topOfBand * HYSTERESIS_TOP_MULTIPLIER) {
152 | return true;
153 | }
154 | } else if (average < bottomOfBand * HYSTERESIS_BOTTOM_MULTIPLIER) {
155 | return true;
156 | }
157 | return false;
158 | }
159 |
160 | /**
161 | * Resets the bandwidth average for this instance of the bandwidth manager.
162 | */
163 | public void reset() {
164 | if (mDownloadBandwidth != null) {
165 | mDownloadBandwidth.reset();
166 | }
167 | mCurrentBandwidthConnectionQuality.set(ConnectionQuality.UNKNOWN);
168 | }
169 |
170 | /**
171 | * Get the ConnectionQuality that the moving bandwidth average currently represents.
172 | * @return A ConnectionQuality representing the device's bandwidth at this exact moment.
173 | */
174 | public synchronized ConnectionQuality getCurrentBandwidthQuality() {
175 | if (mDownloadBandwidth == null) {
176 | return ConnectionQuality.UNKNOWN;
177 | }
178 | return mapBandwidthQuality(mDownloadBandwidth.getAverage());
179 | }
180 |
181 | private ConnectionQuality mapBandwidthQuality(double average) {
182 | if (average < 0) {
183 | return ConnectionQuality.UNKNOWN;
184 | }
185 | if (average < DEFAULT_POOR_BANDWIDTH) {
186 | return ConnectionQuality.POOR;
187 | }
188 | if (average < DEFAULT_MODERATE_BANDWIDTH) {
189 | return ConnectionQuality.MODERATE;
190 | }
191 | if (average < DEFAULT_GOOD_BANDWIDTH) {
192 | return ConnectionQuality.GOOD;
193 | }
194 | return ConnectionQuality.EXCELLENT;
195 | }
196 |
197 |
198 | /**
199 | * Accessor method for the current bandwidth average.
200 | * @return The current bandwidth average, or -1 if no average has been recorded.
201 | */
202 | public synchronized double getDownloadKBitsPerSecond() {
203 | return mDownloadBandwidth == null
204 | ? -1.0
205 | : mDownloadBandwidth.getAverage();
206 | }
207 |
208 | /**
209 | * Interface for listening to when {@link com.facebook.network.connectionclass.ConnectionClassManager}
210 | * changes state.
211 | */
212 | public interface ConnectionClassStateChangeListener {
213 | /**
214 | * The method that will be called when {@link com.facebook.network.connectionclass.ConnectionClassManager}
215 | * changes ConnectionClass.
216 | * @param bandwidthState The new ConnectionClass.
217 | */
218 | public void onBandwidthStateChange(ConnectionQuality bandwidthState);
219 | }
220 |
221 | /**
222 | * Method for adding new listeners to this class.
223 | * @param listener {@link ConnectionClassStateChangeListener} to add as a listener.
224 | */
225 | public ConnectionQuality register(ConnectionClassStateChangeListener listener) {
226 | if (listener != null) {
227 | mListenerList.add(listener);
228 | }
229 | return mCurrentBandwidthConnectionQuality.get();
230 | }
231 |
232 | /**
233 | * Method for removing listeners from this class.
234 | * @param listener Reference to the {@link ConnectionClassStateChangeListener} to be removed.
235 | */
236 | public void remove(ConnectionClassStateChangeListener listener) {
237 | if (listener != null) {
238 | mListenerList.remove(listener);
239 | }
240 | }
241 |
242 | private void notifyListeners() {
243 | int size = mListenerList.size();
244 | for (int i = 0; i < size; i++) {
245 | mListenerList.get(i).onBandwidthStateChange(mCurrentBandwidthConnectionQuality.get());
246 | }
247 | }
248 | }
--------------------------------------------------------------------------------