17

For my instrumentation tests I am using Robotium. Mostly I am able to test everything but offline cases.

As soon as I disable data (using adb, F8 shortcut in emulator, etc. ...) the test disconnects. It goes on in the device/emulator but no results are reported.

So, I have got an idea to put just the app in offline mode and not the whole device. The problem is I don't know how...

Using iptablesApi I would need to root my device. I have read that Mobiwol app uses some kind of a VPN to restrict apps internet access without the need of rooting a device.

Question How does Mobiwol app blocks the internet connection per application? Or is there another way how to test apks offline?

EDIT 12/30/2014

I forgot to say that I am able to run tests offline but I have to start tests when the device is in offline state. Currently, I divided my tests into OFFLINE and ONLINE ones. After running ONLINEs I execute the famous adb kill-server and adb start-server. After that I execute OFFLINEs.

Amio.io
  • 20,677
  • 15
  • 82
  • 117
  • 1
    what exactly are you trying to do in the OFFLINE scenarios? Are you trying to hit a external server? – user2511882 Jan 02 '15 at 11:51
  • 1
    Yes, I am trying to hit an external server and then handle the exceptions, e.g. I have an empty list modification that shows "no internet" connection or some functionality should not pop up when the app is offline, etc. – Amio.io Jan 03 '15 at 10:52

7 Answers7

11

Just making a few suggestions since there seem to be different questions here. 1) If all you want to do is turn off the data before running the OFFLINE test case you might want to simply try using robotium itself to do so..

Example: For WiFi:

WifiManager wifi=(WifiManager)solo.getCurrentActivity().getSystemService(Context.WIFI_SERVICE);
wifi.setWifiEnabled(false);

For Mobile Data(using reflections):

ConnectivityManager dataManager=(ConnectivityManager)solo.getCurrentActivity().getSystemService(Context.CONNECTIVITY_SERVICE);

Method dataClass = ConnectivityManager.class.getDeclaredMethod(“setMobileDataEnabled”, boolean.class);
dataClass.setAccessible(true);
dataClass.invoke(dataManager, true);

You can do the two above calls in the setup() method before running the individual test case in the OFFLINE suite. Once all the test case in the OFFLINE suite are done with you can enable the WiFi/DATA back on in the teardown() method at the very end.

2) Looking at the app that you posted in the OP, it seems pretty much that it:

  • Uses the ipTables based on the OS version

  • Creates a script header based on the UID's for all the applications that need WiFi/Data

  • Should be getting the list of installed apps on the device along with any hidden apps etc from the package manager.

  • And again executes scripts based on user selection for black list and overrides the existing rules in the ipTable with the user desired rules.

Pretty sure though must have been quite hard to code all of that..Sounds much easier in the form of bullet points.

Hope this helps you somewhat.

P.S: If you do figure out something please post an updated answer, would like to know how did you make it work

Update: Make sure you have the neccessary permissions for setting the WiFi/Data on/off in your application manifest. NOT the test apk manifest. IT HAS TO BE THE APPLICATION MANIFEST ITSELF. There is this library which might help you. Its an extension to solo. http://adventuresinqa.com/2014/02/17/extsolo-library-to-extend-your-robotium-test-automation/

user2511882
  • 9,022
  • 10
  • 51
  • 59
  • Hey man, glad you write I am eager to check it and hope that the first solution will work. I am going to check it right now! – Amio.io Jan 04 '15 at 10:12
  • Unfortunately, the robotium solo disabling data is also disconnecting the tests. To point 2: I have read that they were using some kind of VPN and not ipTables. Since for ipTables you need to root your device. – Amio.io Jan 04 '15 at 14:00
  • check the edit to the answer. Posted a link to extSolo which might help you – user2511882 Jan 04 '15 at 15:03
  • I realized that but that is not the problem. I was able to switch the data programatically before calling adb from java. My issue is that when I switch off data the tests get disconnected. That means I cannot reconnect to the same tests even after restarting the connection to the emulator. Or is it just my case that the tests get disconnected? Would you mind to try your code with an instrumentation test? Or maybe I could try Genymotion again which allows the use of wifi... Thanks for help. – Amio.io Jan 04 '15 at 15:13
  • with my experience if you try to interfere with the device while the test case is in progress, the robotium test case disconnects for the most part. However since you mentioned that you tried disconnecting data programatically and it still failed makes me thing might be so because robotium might not support that since data access is done via reflections. try the same thing on wifi and see if it works. – user2511882 Jan 05 '15 at 03:38
  • The problem is the android emulator has no wifi. This could maybe work with genymotion but a year ago it didn't have some features i needed. Now I don't have that much time to check. – Amio.io Jan 05 '15 at 13:50
  • just run the test on a real device? – user2511882 Jan 06 '15 at 05:24
  • I don't have one. ;) Well, I have mine but the tests completely changes environment (call logs and contacts are added and removed etc.) so I don't want to sacrify mine. In the future I shall use the tests on devices. – Amio.io Jan 06 '15 at 09:01
  • 3
    This solution cannot be used anymore. Since Android 9, unsigned apps cannot change the WiFi state. Also, the reflection for disabling mobile data doesn't work since Android KitKat (if I remember well) – Lorenzo Vincenzi Jan 08 '20 at 15:26
7

After spending hours trying to do it similar to user2511882s solution, I still had an exception, because of missing permissions (yes the "modify system settings" permission was activated).

I ended up doing it with UI automator:

public static void setAirplaneMode(boolean enable)
{
    if ((enable ? 1 : 0) == Settings.System.getInt(getInstrumentation().getContext().getContentResolver(),
            Settings.Global.AIRPLANE_MODE_ON, 0))
    {
        return;
    }
    UiDevice device = UiDevice.getInstance(getInstrumentation());
    device.openQuickSettings();
    // Find the text of your language
    BySelector description = By.desc("Airplane mode");
    // Need to wait for the button, as the opening of quick settings is animated.
    device.wait(Until.hasObject(description), 500);
    device.findObject(description).click();
    getInstrumentation().getContext().sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
}

You will need the ACCESS_NETWORK_STATE in your androidTest manifest file:

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Don't forget to disable it after the test.

If you have other languages than english, you need to change "Airplane mode" to the text of your language. As I have several translations, I read it from a ressource string.

Aorlinn
  • 718
  • 5
  • 16
3

There is great library from LinkedIn Test Butler, you can enable, disable both WiFi and mobile data by simply calling:

TestButler.setGsmState(false);
TestButler.setWifiState(false);

The main advantage of this library is that it does not require any permission in your manifest, for more details please refer to project website:

https://github.com/linkedin/test-butler

gingo
  • 3,149
  • 1
  • 23
  • 32
1

Sorry if I'm oversimplifying this, but what about just putting the phone/emulator in airplane mode? Through the actual user interface. That's what I do to test offline cases.

Tamby Kojak
  • 2,129
  • 1
  • 14
  • 25
  • 3
    That won't help. I want to automate the tests completely. I am able to switch off internet from java. The problem is that when I switch it off (doing the thing u've suggested too or any other) the tests get disconnected and are stopped as soon as this happens. Then I am not able to receive test results although the tests are still running on the disconnected device. If I could prevent JUnits from getting stopped when a device/emulator switches to flight mode, I could reconnect to the device and continue testing. – Amio.io Dec 30 '14 at 10:54
1

Here is a solution that uses UiAutomator to enable or disable "Aeroplane mode" from the drop-down status bar, which turns off all networking if enabled. It works on most Android OS versions, and it uses parts of the answer from user Aorlinn (12 October 2019).

app/build.gradle

dependencies {
    androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0"
}

AndroidManifest.xml

No extra permissions are needed, not even <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> , because UiAutomator is only clicking on a button in the status bar UI. Therefore the app does not need direct access to modify the device's Wifi or mobile data settings.

Kotlin

import android.os.Build
import android.provider.Settings
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObjectNotFoundException
import androidx.test.uiautomator.Until
import org.hamcrest.Matchers.`is`
import org.junit.Assert
import org.junit.Assume.assumeNoException
import org.junit.Assume.assumeThat
import java.io.IOException


private var airplaneModeOn = 0
private val OFF = 0
private val ON = 1
private var airplaneModeButtonPosition: Point? = null

/**
 * Turn off the Internet connectivity by switching "Aeroplane mode" (flight mode) on. From:
 * https://stackoverflow.com/questions/27620976/android-instrumentation-test-offline-cases#68956544
 */
@Test
fun checkAppWillWork_withNoInternetConnection() {
    try {
        airplaneModeOn = getCurrentAirplaneModeSetting()
    } catch (e: Exception) {
        e.printStackTrace()
        assumeNoException("Cannot retrieve device setting of 'Airplane Mode'. Aborting the test.", e)
    }

    // Check that Airplane mode is off
    assertThat(airplaneModeOn, `is`(OFF))

    // Press the "Aeroplane mode" button on the 'Quick Settings' panel
    toggleAeroplaneModeButton()

    // Verify Airplane mode is on
    assumeThat("Cannot change the 'Aeroplane Mode' setting. Test aborted.", airplaneModeOn, `is`(ON))
    assertThat(airplaneModeOn, `is`(ON))

    // Do some tests when the Internet is down
    // Note: Switch the Internet back on in cleanup()
}

/**
 * Turn the Internet connectivity on or off by pressing the "Aeroplane mode" button. It opens the
 * status bar at the top, then drags it down to reveal the buttons on the 'Quick Settings' panel.
 */
@Throws(UiObjectNotFoundException::class)
private fun toggleAeroplaneModeButton() {
    try {
        airplaneModeOn = getCurrentAirplaneModeSetting()
    } catch (e: SecurityException) {
        e.printStackTrace()
        Assert.fail()
    } catch (e: IOException) {
        e.printStackTrace()
        Assert.fail()
    }

    // Open the status bar at the top; drag it down to reveal the buttons
    val device = UiDevice.getInstance(getInstrumentation())
    device.openQuickSettings()

    // Wait for the button to be visible, because opening the Quick Settings is animated.
    // You can use any string here; You only need a time delay wait here.
    val description = By.desc("AeroplaneMode")
    device.wait(Until.hasObject(description), 2000)

    // Search for and click the button
    var buttonClicked = clickObjectIfFound(device,
            "Aeroplane mode", "Airplane mode", "機内モード", "Modo avión")
    if (!buttonClicked) {
        // Swipe the Quick Panel window to the LEFT, if possible
        val screenWidth = device.displayWidth
        val screenHeight = device.displayHeight
        device.swipe((screenWidth * 0.80).toInt(), (screenHeight * 0.30).toInt(),
                     (screenWidth * 0.20).toInt(), (screenHeight * 0.30).toInt(), 50)
        buttonClicked = clickObjectIfFound(device,
                "Aeroplane mode", "Airplane mode", "機内モード", "Modo avión")
    }
    if (!buttonClicked) {
        // Swipe the Quick Panel window to the RIGHT, if possible
        val screenWidth = device.displayWidth
        val screenHeight = device.displayHeight
        device.swipe((screenWidth * 0.20).toInt(), (screenHeight * 0.30).toInt(),
                     (screenWidth * 0.80).toInt(), (screenHeight * 0.30).toInt(), 50)
        clickObjectIfFound(device,
                "Aeroplane mode", "Airplane mode", "機内モード", "Modo avión")
    }

    // Wait for the Internet to disconnect or re-connect
    device.wait(Until.hasObject(description), 6000)

    // Close the Quick Settings panel
    getInstrumentation().context
            .sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))

    // Verify change in device settings
    try {
        airplaneModeOn = getCurrentAirplaneModeSetting()
    } catch (e: SecurityException) {
        e.printStackTrace()
        Assert.fail()
    } catch (e: IOException) {
        e.printStackTrace()
        Assert.fail()
    }
}

/**
 * On Android 8/9, use an 'adb shell' command to retrieve the "Airplane Mode" setting, like this:
 *
 *
 * `adb shell settings list global | grep airplane_mode_on   ==>   "airplane_mode_on=0"`
 *
 *
 * (But note that grep is not available on the Android shell. See guidance URLs below)
 *
 *
 *  * https://www.reddit.com/r/tasker/comments/fbi5ai/psa_you_can_use_adb_to_find_all_the_settings_that/
 *  * https://stackoverflow.com/questions/33970956/test-if-soft-keyboard-is-visible-using-espresso
 *
 *
 * On all other Android OS versions, use `Settings.System.getInt()` to retrieve the "Airplane Mode" setting.
 * It sets `airplaneModeOn = 1 (true)` or `airplaneModeOn = 0 (false)`
 *
 * @throws IOException if `executeShellCommand()` didn't work
 * @throws SecurityException if `Settings.System.getInt()` didn't work
 */
@Throws(IOException::class, SecurityException::class)
private fun getCurrentAirplaneModeSetting(): Int {
    if (Build.VERSION.SDK_INT in 26..28 /*Android 8-9*/) {
        val shellResponse = UiDevice
                .getInstance(getInstrumentation())
                .executeShellCommand("settings list global")

        airplaneModeOn = when {
            shellResponse.contains("airplane_mode_on=1") -> 1
            shellResponse.contains("airplane_mode_on=0") -> 0
            else -> throw IOException("Unsuitable response from adb shell command 'settings list global'")
        }

    } else {
        // Oddly this causes a SecurityException on Android 8,9 devices
        airplaneModeOn = Settings.System.getInt(
                getInstrumentation().context.contentResolver,
                Settings.Global.AIRPLANE_MODE_ON,
                0)
    }
    return airplaneModeOn
}

/**
 * Make UiAutomator search for and click a single button, based on its text label.
 *
 * Sometimes multiple buttons will match the required text label. For example,
 * when Airplane mode is switched on, "Mobile data Aeroplane mode" and "Aeroplane mode"
 * are 2 separate buttons on the Quick Settings panel, on Android 10+.
 * For Aeroplane mode, always click on the last matched item.
 *
 * @param textLabels You must supply the language variants of the button that you want to click,
 * for example "Cancel" (English), "Cancelar" (Spanish), "취소" (Korean)
 * @return True if a button was found and clicked, otherwise return false
 */
private fun clickObjectIfFound(device: UiDevice, vararg textLabels: String): Boolean {
    for (languageVariant in textLabels) {
        val availableButtons = device.findObjects(By.text(languageVariant))
        if (availableButtons.size >= 1) {
            if (airplaneModeButtonPosition == null) {
                availableButtons[availableButtons.size - 1].click()
                airplaneModeButtonPosition = availableButtons[availableButtons.size - 1].visibleCenter
                return true
            } else {
                // Use the stored position to avoid clicking on the wrong button
                for (button in availableButtons) {
                    if (button.visibleCenter == airplaneModeButtonPosition) {
                        button.click()
                        airplaneModeButtonPosition = null
                        return true
                    }
                }
            }
        }
    }
    return false
}

@After
fun cleanup() {
    // Switch the Internet connectivity back on, for other tests.
    if (airplaneModeOn == ON) {
        toggleAeroplaneModeButton()
        assertThat(airplaneModeOn, `is`(OFF))
    }
}

You will have to adjust the string "Aeroplane mode" if your device is in a different language to English. For example, you could check for about 70 language translations here: https://github.com/aosp-mirror/platform_frameworks_base/search?q=global_actions_toggle_airplane_mode

Mr-IDE
  • 7,051
  • 1
  • 53
  • 59
  • Personally I think it would be better to just enable the `ACCESS_NETWORK_STATE` and use Aorlinn's solution to check it programmatically via `Settings.System`, rather than trying to parse the output of a shell command – Adam Burley Aug 07 '23 at 11:14
0

Use this solution from @Illyct (please upvote their answer https://stackoverflow.com/a/64765567/191761)

InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand("svc wifi disable")
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand("svc data disable")
Adam Burley
  • 5,551
  • 4
  • 51
  • 72
-1

Due to @user2511882 answer you can use Application context instead of Activity in Android X Test via:

internal fun switchWifi(value: Boolean) {
val wifiManager = ApplicationProvider.getApplicationContext<YourApplicationClass>().getSystemService(Context.WIFI_SERVICE) as WifiManager
wifiManager.isWifiEnabled = value}

Consider that this approach only works on API <= 28. There are other approaches like using UI Automator or API > 28

Sepehr
  • 960
  • 11
  • 17