I've been working for a while around a lack of ability to restrict Crashlytics network usage under certain conditions. For example - on roaming, on metered networks and so on..
According to SDK documentation, only two options I found addressing somehow this:
"Opt Out" on runtime by simply not initialize Crashlytics
built-in User consent dialog before sending a crash report
This API's are very limited, because:
Not initialize Crashlytics not only prevents network access but also prevents any chance Crashlytics will save locally the crash report so that eventually the event will be sent. Not to mention there is no good way to opt out in runtime, besides overriding brutally the Thread.setUncaughtExceptionHandler
consent dialog not making any sense to the user if a crash happens in the background.
My question basically:
Am I missing something?
Is there any way to restrict Crashlytics network access?
My motivation comes from a need to prevent situation my app uses network bandwidth potentially can cost money to the user under certain conditions, although "cellular network" or "use data over roaming" device settings are enabled.
There is two step process which we are using in our app, this is not using Mobile Network and also not related to roaming as well.
Saving crash logs to file in app data partition i.e. on device:
Refer to this link
Upload crash data to server when WiFi network is connected:
public class ConnectivityStatusReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
final ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
if (activeNetworkInfo != null && activeNetworkInfo.getTypeName() == "WIFI") {
// post your crash logs to server
}
}
}
There is not a way to restrict the internet usage for Crashlytics in an application. But how I would fix it is to either give the user information that Crashlytics is using roaming or just save the crash report locally and send them once the user in connected with a wifi network. Also you could give the user the choice if he prefers to save the crash reports locally or send them right away over roaming.
Save the ErrorLog locally on the device
Upload the ErrorLog once a connection with a wifi is established
You should be able to use the ConnectivityManager to get the state of the Wi-Fi adapter. From there you can check if it is connected or even available.
ConnectivityManager connManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mWifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
if (mWifi.isConnected()) {
// post error logs
}
I'm the former maintainer of the Crashlytics SDK for iOS/macOS. I'm relatively unfamiliar with the Android version of the SDK, and definitely unfamiliar with Android in general. But, I'll give this a shot.
What you want to do is something that has been requested on the iOS side a few times. I would have loved to do it actually, because it seems pretty terrible to force end-users to incur these costs. However, the iOS SDK's networking and start up routine are both very complex and very delicate. It is highly challenging to guarantee that crashes are delivered and that there are zero possibilities for inconsistent states. I believe that Android is simpler here, but I cannot say this with authority.
The iOS SDK, however, does have some hooks for additional client-level functionality. Check out the warning around one of those APIs:
* #warning Just implementing this delegate method will disable all forms of synchronous report submission. This can
* impact the reliability of reporting crashes very early in application launch.
Basically, in order to satisfy the contract of this particular API, some techniques to improve reporting reliability have to be disabled. The thing is, sometimes it's worth it. Lots of apps decide to make this tradeoff. Many apps also delay initializing Crashlytics to eek out extra performance. This has a huge impact on reporting reliability, but that's another tradeoff app developers have to make.
I think you should seriously consider just not enabling Crashlytics in these situations, if you can easily detect them. Maybe Android even allows end-users to do this on a per-app basis? In that case, you'd never get any reports anyways. I would imagine that your user base is diverse enough that missing some reports in these situations wouldn't be that terrible. Or, perhaps you'd like to surface it as a user-facing option.
You could even do something totally crazy, like override Thread.setUncaughtExceptionHandler yourself, and buffer up exceptions during this situation to disk. And then, replay them to Crashlytics when things are better. Turn it into an open source lib. I bet people will love it! Possibly not the Crashlytics' Android team though ;) (Hi!)
This is also basically the same recommendation Gastón offered above, with just some extra context around what I've seen on the iOS side. Also shoot the Crashlytics people an email asking for this. I think it's a great idea.
I was reading the docs at fabric and I just found something interesting:
Crashlytics processes exceptions on a dedicated background thread, so
the performance impact to your app is minimal. To reduce your users’
network traffic, Crashlytics batches logged exceptions together and
sends them the next time the app launches.
So I was thinking about a workaround since the crashes without network are being sent when the app is initialized, you could prompt any dialog to the user at startup telling if they want to connect to the internet to send crash reports to solve current problems in the app. ( so you are using their network data with the user consent)
The thing here is we don't know how to stop crashlytics from sending this reports, they will store it at the device if the device is offline and send it back after the device is just with connection again as it states here
Another way out could be just log important fatal issues with the custom login they offer and just send them, you can find more about it here
To make sure that sending crash reports has the smallest impact on
your user’s devices, Crashlytics logs have a maximum size of 64 KB.
When a log exceeds 64 KB, the earliest logged values will be dropped
in order to maintain this threshold.
In conclusion, after reading the docs, there is no way to disable Crashlytics to constantly send reports, you can only manage the network connection of the user when you want them to send or not reports. It's like connectivity is the switch on and off of Crashlytics at the moment.
It just talks about "Reducing network traffic" but not about disabling Crashlytics network at all.
Another way that comes to my head is to make a flag for starting crashlytics, and then use inside a condition Crashlytics.start()
When you want to disable it just do the following
CrashlyticsCore core = new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build();
Fabric.with(this, new Crashlytics.Builder().core(core).build());
Playing with these two things is the only way I think it's possible to reduce network usage of Crashlytics at the moment.
You can restrict Crashlytics network usage by a static field.
Define a static global variable, according to its value write logic for your Crashlytics.
private static boolean INROAMING = false;
Now you can use below logic for your purpose. Like don't provide co
if(isInternetIsConnected(this).equals("MOBILE")){
if(INROAMING){
//write your logic for context here, when phone is in roaming
//restrict logic for crashlytics
}else{
//write your logic for context herem, when phone is not in roaming
//un-restrict logic for crashlytics
}
}
public boolean checkForRoaming() {
final TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
PhoneStateListener phoneStateListener = new PhoneStateListener() {
#Override
public void onServiceStateChanged(ServiceState serviceState) {
super.onServiceStateChanged(serviceState);
if (telephonyManager.isNetworkRoaming()) {
// In Roaming
INROAMING = true;
} else {
// Not in Roaming
INROAMING = false;
}
// You can also check roaming state using this
if (serviceState.getRoaming()) {
// In Roaming
INROAMING = true;
} else {
// Not in Roaming
INROAMING = false;
}
}
};
}
public String isInternetIsConnected(Context context) {
try {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
assert cm != null;
#SuppressLint("MissingPermission") NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
if (activeNetwork != null) { // connected to the internet
if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) {
// connected to wifi
return "WIFI";
} else if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) {
// connected to the mobile provider's data plan
checkForRoaming();
return "MOBILE";
}
} else {
// not connected to the internet
return "NO CONNECTION";
}
} catch (Exception e) {
e.printStackTrace();
}
return "NO CONNECTION";
}
}
There is not a way to restrict the internet usage for Crashlytics in an application. You can give choice to user, if he prefers to save the crash reports locally or send them right away over roaming.
Save the ErrorLog locally on the device
Upload the ErrorLog once a connection with a wifi is established.
You can use ConnectivityManager to get the state of the network. You can check if it is connected or even available.
ConnectivityManager connManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mWifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
if (mWifi.isConnected()) {
// send error logs
}
Above code you can add in broadcastreceiver which will notify connection
Example:
public class ConnectivityStatusReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager connManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mWifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
if (mWifi.isConnected()) {
// send error logs
}
}
}
Related
I have noticed that after turning off the internet connection and than turning it back on (while my Android app is still running, in the background or not), it takes the Firestore module a pretty long time to regain the connection to the server (about a minute), and I can't make any Firestore operations until the connection is regained.
Is this a normal behavior?
If it does, can I somehow check the Firestore module current connection status? (in order to limit my user's actions if there is a need).
As far as i know, there is no equivalent to Firebase Realtime Database's .info/connected in Cloud Firestore, that allows you to verify the connection status. I read on Firebase offical blog a post regarding the differences between Firebase and Firestore and i saw that this problem is in fact one of the use-cases.
The Realtime Database has native support for presence -- that is, being able to tell when a user has come online or gone offline. While we do have a solution for Cloud Firestore, it's not quite as elegant.
If you read Firestore offical documentation, you will see that there is a possible implementation of a presence system by combining Realtime Database and Firestore.
I'm currently testing a fix for this that seems to be working.
Step 1: Ensure the device has a data connection using
Android's recommended approach
public static boolean isConnectedToInternet(#NonNull Context _context) {
ConnectivityManager cm = (ConnectivityManager)_context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null)
return false;
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return activeNetwork != null && activeNetwork.isConnectedOrConnecting();
}
Step 2: Force Firestore to connect (the idea is to override Firestore's internal connection retry exponential backoff)
FirebaseFirestore.getInstance().enableNetwork()
Step 3 (optional): If you're doing this when your app is in the background, you may also want to use Android's Task API to synchronously wait until the enableNetwork() Task completes. Otherwise, the OS may think your background work is complete, and cut off your app's access to system resources (network, CPU, etc).
try { Tasks.await(FirebaseFirestore.getInstance().enableNetwork()); }
catch (Exception e) {
Log.e(TAG, "error in Firestore enableNetwork(): ", e);
return;
}
So this is quite a revelation - the fact that Android SDK does not seem to have a reliable way to check for internet access.
Allow me to clarify.
Android documentation & most online samples use an approach similar to one found here: https://stackoverflow.com/a/4009133/980917
Something similar to:
public boolean isOnline() {
ConnectivityManager cm =
(ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = cm.getActiveNetworkInfo();
return netInfo != null && netInfo.isConnectedOrConnecting();
}
However - this just checks if you are connected to a network and does not guarantee that you can reach the internet.
For those that are not aware of this, its quite easy to reproduce by unplugging the "IN" Ethernet cable from your router or connecting to any Wifi that is not connected to the internet.
KNOWN WORKAROUNDS:
1) Try to reach a web site: https://stackoverflow.com/a/5803489/980917
This is problematic as needs Thread management.
2) Try to ping Google's DNS: https://stackoverflow.com/a/27312494/980917
Does not need to be wrapped in a Thread but will freeze UI.
What I'm looking for:
Ideally I would like to have an internet check as a simple static method in a utility class that can be called from UI or business logic code. Because of this above solutions dont really work well as require additional overhead in the caller class to handle Thread management.
Is there any way to get a reliable internet check in Android w/o polluting the caller class with thread management code ?
Thanks.
I'd made a special class for this with a set of static mehods. It has the well know and shown in your question method to detect if Network is available. It also has a method to check if the target server/host is reachable via this network - checkIsServerReachable(). However since its a networking task it can't be run on UI thread so the only way here is to use AsyncTask, Thread or callback.
Is there a way to access Android's broadcast that the WiFi connection is currently a captive portal (requires web login)? Android seems to do have this built in. If not a broadcast receiver, is there a way to check for the result of the captive portal check? I believe it's using this class, which is hidden from the API:
http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.L_preview/android/net/CaptivePortalTracker.java
Prior to 4.2, it was probably using:
http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.1.2_r1/android/net/wifi/WifiWatchdogStateMachine.java
Background:
I had been using my own method to detect whether WiFi likely required a login. I would wait for a WiFi connected state, then ping a site and make sure there was a response. This seemed to work great in most cases. Another strategy is to do a HttpRequest and check for a redirect or the response body you receive back, similar to Android's strategy in the classes listed above.
However, new to Lollipop is that the mobile data connection is used when WiFi does not have connectivity. This means my ping method will still return results, and that a redirect would not happen, as the request would be routed over the mobile data.
Is there a way to get Android's current status of a WiFi captive portal? If not, can we make sure a request goes over WiFi even when there's no connectivity as seen by Android?
You'd have to use the new ConnectivityManager.setProcessDefaultNetwork API to force your app to communicate over the captive portal. See https://github.com/pawitp/muwifi-autologin/commit/f045fe36f1fd98a106ea652e2d56f7ddfc871760 for an example.
Complete code added:
final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
for (Network net : cm.getAllNetworks()) {
if (cm.getNetworkInfo(net).getType() == ConnectivityManager.TYPE_WIFI) {
Utils.logDebug(TAG, "Seting process network to " + net);
/*Since API 23 ConnectivityManager.setProcessDefaultNetwork(net);
is deprecated, use: */
cm.bindProcessToNetwork(net);
}
}
As of Android 4.1, your device can detect if it's connected to a mobile hotspot (given that the mobile hotspot is also running Android 4.1 or higher). Also, you have the option to flag networks as mobile hotspots (under Settings / Data Usage / Overflow menu / Mobile Hotspots).
But how do I detect this as a -user- I meant developer? It's not stored in the WifiConfiguration, so where is it?
Some context: I want to build a simple tool for Android that checks if you are connected to a network that you or Android has flagged as a mobile hotspot. If so, it will check if no other (non-hotspot) networks are available. If so, it should connect to these other networks since those should be much faster and have no data cap. Why? Because my phones and tablets connect to (mobile) hotspots quite often, even when a better network is available.
Here is some pseudo code of what I'm looking for:
// Check if android has detected mobile hotspot
WifiManager wifiMgr = getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiMgr .getConnectionInfo();
boolean isMobileHotspot = wifiInfo.isMobileHotspot;
UPDATE Jul 3rd 2014
Okay so Matiash' answer is good but ConnectivityManager.isActiveNetworkMetered() will only return the value for the current network. I do need that, so it helped me along, but it bring me to the next part in my tool/app:
IF the device is connected to a mobile hotspot (or a 'metered network' as Android calls it) I want to check if any of the nearby access points is a better option. So I need to know whether any of the known AP's (WifiManager.getConfiguredNetworks()) is also flagged as such before I connect to it...
I have a List<ScanResult> and a List<WifiConfiguration>, looks like neither of them has this information.
Which bring me back to my initial question: Is there a way to retrieve the Mobile Hotspots (as configured by Android and/or user) under Data Usage? And this time I mean ALL of them.
UPDATE Jul 7th 2014
I've posted a feature request in the AOSP Issue Tracker for access (readonly) to the NetworkPolicyManager. Plz vote on it here: https://code.google.com/p/android/issues/detail?id=73206&thanks=73206&ts=1404719243
You can access this information by calling ConnectivityManager.isActiveNetworkMetered().
This will return whether the active connection is a hotspot (as defined in Data Usage -> Mobile Hotspots).
About the second part, I'm sorry but I don't think that's possible. The flag is not public, and even if you get the object that could be used to retrieve it (android.net.NetworkPolicyManager) by reflection:
Object npm = Class.forName("android.net.NetworkPolicyManager").getDeclaredMethod("from", Context.class).invoke(null, this);
Object policies = npm.getClass().getDeclaredMethod("getNetworkPolicies").invoke(npm);
calling getNetworkPolicies() requires the MANAGE_NETWORK_POLICY permission, which cannot be obtained by non-system apps, because it has a "signature" protection level.
I hope to be proved incorrect though. :) Maybe looking at the source code of the Android activity that manages this information (https://github.com/android/platform_packages_apps_settings/blob/master/src/com/android/settings/net/DataUsageMeteredSettings.java), in particular the buildWifiPref() method, will provide some clue.
I do not know if what you want is possible but you can check whether your device is connected to a network by checking the ip.
You can use the tool below to see if you has ip, and shalt know if he is connected to a network or not.
public static Boolean check_connection(final Context _context)
{
boolean connected;
ConnectivityManager conectivtyManager = (ConnectivityManager) _context
.getSystemService(Context.CONNECTIVITY_SERVICE);
if (conectivtyManager.getActiveNetworkInfo() != null
&& conectivtyManager.getActiveNetworkInfo().isAvailable()
&& conectivtyManager.getActiveNetworkInfo().isConnected())
{
connected = true;
} else
{
connected = false;
}
return connected;
}
//Check if hotspot tethering is enabled
try {
ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
boolean isMobileData = connectivityManager.isActiveNetworkMetered();
if(isMobileData) {
List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface networkInterface : interfaces) {
if (networkInterface.getName().equals("ap0")) {
//Tethering is enabled
SendHotspotEnabledHandler sendHotspotEnabledHandler = new SendHotspotEnabledHandler(new WeakReference<Context>(SendInstalledAppsService.this));
sendHotspotEnabledHandler.execute();
break;
}
}
}
} catch (SocketException e) {
}
I have check for internet connectivity that goes like this:
public static boolean isInternetAvailable(Context ctx)
{
NetworkInfo info = ((ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
if (info == null || !info.isConnected()) return false;
if (info.isRoaming())
{
return Preferences.getIsRoamingAllowed(ctx);
}
return true;
}
It's been working good for all installations so far. Today user came in with a phone where everything worked (browser, email, etc) and it wasn't roaming. But my app due to this check was giving "No connection" error.
Phone is HTC Droid ERIS with 2.1
Anyone saw this issue? Any pointers why?
Ok I have written a test application that gets the activenetwork and all networks and lets me see what is happening, I am about to go out and test this since the anomalies I am seeing happen when switching from one network to the other (as in when you go out of wifi range and into cdma etc)
Couple of things that might help regardless first you can change info.isConnected to the following
if (info == null || !info.isConnectedOrConnecting()) return false;
This makes it a little more robust in that if you are in the middle of switch over you still let the user logon
Second thing is that you said that your app denied login if roaming because your apps allow roaming preference was set to false
return Preferences.getIsRoamingAllowed(ctx);
I think you need to follow a different pattern (just my opinion) first because If the user has disallowed roaming via their settings (phone not your app) and they are on a roaming network then the .getActiveNetwork() will return null, or not connected or not available (in which case the .getReason returns "noroaming")
Personally I would let the phone decide, but if you need to restrict it then he pattern I would follow would be
Set the default to true, but note that it's the first time your activity has started and the user has had no chance to set anything (since no pref's are set this should be easy enough to detect) Detect your network connection, if you have one then also note if they are roaming
Prompt them with an alert dialog which warns them they are currently roaming and Ask them if they want to login now or wait until later
OR
Normal Login if they are not roaming
But in either case offer them the ability to set the "roaming" option the first time instead of having them figure it out themselves.
That would address your catch 22 situation and save you some phone calls, anyway that's a design decision but I think it would work out better for you than your current pattern.
Also I think instead of just telling them there is no connection you might want to tell them why, return an enum instead of a boolean and then format dependent on that.
Finally I am going to test a bit more before my final answer because I am seeing some oddity's in the network state but want to confirm my findings before giving you the results, I need this as well so it was a good time for me to dig into this.