# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import json
import os
import sys
import posixpath
import shutil
import tempfile
import time
import datetime
import signal
import thread

from marionette import MarionetteTestCase, B2GTestCaseMixin
from marionette_driver import expected, By, Wait
from marionette_driver.errors import NoSuchElementException, StaleElementException
import mozfile

from environment import GaiaTestEnvironment
from file_manager import GaiaDeviceFileManager, GaiaLocalFileManager

DEFAULT_SETTINGS = {
    'airplaneMode.enabled': False,  # disable airplane mode
    'audio.volume.alarm': 0,  # mute alarm audio
    'audio.volume.content': 0,  # mute content audio
    'audio.volume.notification': 0,  # mute audio notifications
    'camera.sound.enabled': False,  # mute camera sounds
    'edgesgesture.enabled': False,  # disable edge gestures
    'ftu.manifestURL': None,  # disable the first time usage app
    'keyboard.autocorrect': False,  # disable auto-correction of keyboard
    'keyboard.clicksound': False,  # mute keyboard click sound
    'keyboard.enabled-layouts': str({
        'app://keyboard.gaiamobile.org/manifest.webapp': {
            'en': True, 'number': True}}),  # reset keyboard layouts
    'keyboard.vibration': False,  # disable keyboard vibration
    'language.current': 'en-US',  # reset language to en-US
    'lockscreen.enabled': False,  # disable lockscreen
    'lockscreen.passcode-lock.code': '1111',
    'lockscreen.passcode-lock.enabled': False,  # disable lockscreen passcode
    'lockscreen.unlock-sound.enabled': False,  # mute unlock sound
    'message.sent-sound.enabled': False,  # mute message sent sound
    'phone.ring.keypad': False,  # mute dial pad sounds
    'privacy.donottrackheader.value': -1,  # reset do not track
    'ril.data.roaming_enabled': False,  # disable roaming
    'search.suggestions.enabled': False,  # disable search suggestions
    'screen.brightness': 0.1,  # reduce screen brightness
    'screen.timeout': 0,  # disable screen timeout
    'vibration.enabled': False,  # disable vibration
    'privacy.trackingprotection.shown': True
}

DEFAULT_PREFS = {
    'webapps.update.enabled': False,  # disable web apps update
    'ui.caretBlinkTime': 0,  # Make caret permanently visible so imagecompare screenshots are consistent
    'layers.screen-recording.enabled': True # Enable the screen recording by default so it can be triggered
}


class GaiaApp(object):

    def __init__(self, origin=None, name=None, frame=None, src=None, manifest_url=None, entry_point=None):
        self.frame = frame
        self.frame_id = frame
        self.src = src
        self.name = name
        self.origin = origin
        self.manifest_url = manifest_url
        self.entry_point = entry_point

    def __eq__(self, other):
        return self.__dict__ == other.__dict__


class GaiaApps(object):

    def __init__(self, marionette):
        self.marionette = marionette
        js = os.path.abspath(os.path.join(__file__, os.path.pardir, 'atoms', "gaia_apps.js"))
        self.marionette.import_script(js)

    def get_permission(self, app_name, permission_name):
        self.marionette.switch_to_frame()
        return self.marionette.execute_async_script("return GaiaApps.getPermission('%s', '%s')" % (app_name, permission_name))

    def set_permission(self, app_name, permission_name, value):
        self.marionette.switch_to_frame()
        return self.marionette.execute_async_script("return GaiaApps.setPermission('%s', '%s', '%s')" %
                                                    (app_name, permission_name, value))

    def set_permission_by_url(self, manifest_url, permission_name, value):
        self.marionette.switch_to_frame()
        return self.marionette.execute_async_script("return GaiaApps.setPermissionByUrl('%s', '%s', '%s')" %
                                                    (manifest_url, permission_name, value))

    def launch(self, name, manifest_url=None, entry_point=None, switch_to_frame=True, launch_timeout=None):
        self.marionette.switch_to_frame()
        if manifest_url:
            result = self.marionette.execute_async_script("GaiaApps.launchWithManifestURL('%s', %s)"
                                                          % (manifest_url, json.dumps(entry_point)), script_timeout=launch_timeout)
            assert result, "Failed to launch app with manifest_url '%s'" % manifest_url
        else:
            result = self.marionette.execute_async_script(
                "GaiaApps.launchWithName('%s')" % name,
                script_timeout=launch_timeout)
            assert result, "Failed to launch app with name '%s'" % name
        app = GaiaApp(frame=result.get('frame'),
                      src=result.get('src'),
                      name=result.get('name'),
                      origin=result.get('origin'),
                      manifest_url=result.get('manifestURL'),
                      entry_point=result.get('entryPoint'))
        if app.frame_id is None:
            raise Exception("App failed to launch; there is no app frame")
        if switch_to_frame:
            self.marionette.switch_to_frame(app.frame_id)
        return app

    @property
    def displayed_app(self):
        self.marionette.switch_to_frame()
        result = self.marionette.execute_script('return GaiaApps.getDisplayedApp();')
        return GaiaApp(frame=result.get('frame'),
                       src=result.get('src'),
                       name=result.get('name'),
                       origin=result.get('origin'),
                       manifest_url=result.get('manifestURL'),
                       entry_point=result.get('entryPoint'))

    def switch_to_displayed_app(self):
        self.marionette.switch_to_default_content()
        self.marionette.switch_to_frame(self.displayed_app.frame)

    def is_app_installed(self, app_name):
        self.marionette.switch_to_frame()
        return self.marionette.execute_async_script("GaiaApps.locateWithName('%s')" % app_name)

    def kill(self, app):
        self.marionette.switch_to_frame()
        result = self.marionette.execute_async_script("GaiaApps.kill('%s');" % app.origin)
        assert result, "Failed to kill app with name '%s'" % app.name
        # Workaround for bug 1219971, launch an app directly after a kill fails sometimes
        time.sleep(0.5)

    def kill_all(self):
        # First we attempt to kill the FTU, we treat it as a user app
        for app in self.running_apps(include_system_apps=True):
            if app.origin == 'app://ftu.gaiamobile.org':
                self.kill(app)
                break

        # Now kill the user apps
        self.marionette.switch_to_frame()
        self.marionette.execute_async_script("GaiaApps.killAll();")
        # Workaround for bug 1219971, launch an app directly after a kill fails sometimes
        time.sleep(0.5)

    def install(self,manifest_url):
        self._change_state_of_app(manifest_url, 'install')

    def uninstall(self,manifest_url):
        self._change_state_of_app(manifest_url, 'uninstall')

    def install_package(self, manifest_url):
        self._change_state_of_app(manifest_url, 'installPackage')

    def _change_state_of_app(self, manifest_url, desired_action):
        self.marionette.switch_to_frame()
        data_layer = GaiaData(self.marionette)
        preference_action = desired_action.rstrip('Package')
        data_layer.set_bool_pref('dom.mozApps.auto_confirm_{}'.format(preference_action), True)
        result = self.marionette.execute_async_script('GaiaApps.{}("{}");'.format(desired_action, manifest_url))
        assert (result is True), 'Failed to {} app: {}'.format(desired_action, manifest_url)
        data_layer.set_bool_pref('dom.mozApps.auto_confirm_{}'.format(preference_action), False)

    @property
    def installed_apps(self):
        apps = self.marionette.execute_async_script(
            'return GaiaApps.getInstalledApps();')
        result = []
        for app in [a for a in apps if not a['manifest'].get('role')]:
            entry_points = app['manifest'].get('entry_points')
            if entry_points:
                for ep in entry_points.values():
                    result.append(GaiaApp(
                        origin=app['origin'],
                        name=ep['name'],
                        manifest_url=app['manifest']))
            else:
                result.append(GaiaApp(
                    origin=app['origin'],
                    name=app['manifest']['name'],
                    manifest_url=app['manifest']))
        return result

    def running_apps(self, include_system_apps=False):
        '''  Returns a list of running apps
        Args:
            include_system_apps: Includes otherwise hidden System apps in the list
        Returns:
            A list of GaiaApp objects representing the running apps.
        '''
        include_system_apps = json.dumps(include_system_apps)
        self.marionette.switch_to_frame()
        apps = self.marionette.execute_script(
            "return GaiaApps.getRunningApps(%s);" % include_system_apps)
        result = []
        for app in [a[1] for a in apps.items()]:
            # Browser app can have no manifest when url is visited
            manifest_url = None
            if 'manifest' in app:
                manifest_url = app['manifest']
            result.append(GaiaApp(origin=app['origin'], name=app['name'], manifest_url=manifest_url))
        return result


class GaiaData(object):

    def __init__(self, marionette, testvars=None):
        self.marionette = marionette
        self.testvars = testvars or {}
        js = os.path.abspath(os.path.join(__file__, os.path.pardir, 'atoms', "gaia_data_layer.js"))
        self.marionette.import_script(js)

        # TODO Bugs 1043562/1049489 To perform ContactsAPI scripts from the chrome context, we need
        # to import the js file into chrome context too
        self.marionette.set_context(self.marionette.CONTEXT_CHROME)
        self.marionette.import_script(js)
        self.marionette.set_context(self.marionette.CONTEXT_CONTENT)

    def set_time(self, date_number):
        self.marionette.set_context(self.marionette.CONTEXT_CHROME)
        self.marionette.execute_script("window.navigator.mozTime.set(%s);" % date_number)
        self.marionette.set_context(self.marionette.CONTEXT_CONTENT)

    @property
    def all_contacts(self):
        self.marionette.switch_to_frame()
        # TODO Bug 1049489 - In future, simplify executing scripts from the chrome context
        self.marionette.push_permission('contacts-read', True)
        self.marionette.set_context(self.marionette.CONTEXT_CHROME)
        result = self.marionette.execute_async_script('return GaiaDataLayer.getAllContacts();')
        self.marionette.set_context(self.marionette.CONTEXT_CONTENT)
        self.marionette.push_permission('contacts-read', False)
        return result

    @property
    def sim_contacts(self):
        self.marionette.switch_to_frame()
        adn_contacts = self.marionette.execute_async_script('return GaiaDataLayer.getSIMContacts("adn");')
        sdn_contacts = self.marionette.execute_async_script('return GaiaDataLayer.getSIMContacts("sdn");')
        return adn_contacts + sdn_contacts

    def insert_contact(self, contact):
        self.marionette.switch_to_frame()
        # TODO Bug 1049489 - In future, simplify executing scripts from the chrome context
        self.marionette.push_permission('contacts-create', True)
        self.marionette.set_context(self.marionette.CONTEXT_CHROME)
        mozcontact = contact.create_mozcontact()
        result = self.marionette.execute_async_script('return GaiaDataLayer.insertContact(%s);' % json.dumps(mozcontact))
        assert result, 'Unable to insert contact %s' % contact
        self.marionette.set_context(self.marionette.CONTEXT_CONTENT)
        self.marionette.push_permission('contacts-create', False)

    def insert_sim_contact(self, contact, contact_type='adn'):
        self.marionette.switch_to_frame()
        mozcontact = contact.create_mozcontact()
        result = self.marionette.execute_async_script('return GaiaDataLayer.insertSIMContact("%s", %s);'
                                                      % (contact_type, json.dumps(mozcontact)))
        assert result, 'Unable to insert SIM contact %s' % contact
        return result

    def delete_sim_contact(self, moz_contact_id, contact_type='adn'):
        self.marionette.switch_to_frame()
        result = self.marionette.execute_async_script('return GaiaDataLayer.deleteSIMContact("%s", "%s");'
                                                      % (contact_type, moz_contact_id))
        assert result, 'Unable to insert SIM contact %s' % moz_contact_id

    def remove_all_contacts(self):
        self.marionette.switch_to_frame()
        # TODO Bug 1049489 - In future, simplify executing scripts from the chrome context
        self.marionette.push_permission('contacts-write', True)
        self.marionette.set_context(self.marionette.CONTEXT_CHROME)
        timeout = max(self.marionette.timeout or 60000, 1000 * len(self.all_contacts))
        self.marionette.push_permission('contacts-read', True)
        result = self.marionette.execute_async_script('return GaiaDataLayer.removeAllContacts();', script_timeout=timeout)
        assert result, 'Unable to remove all contacts'
        self.marionette.set_context(self.marionette.CONTEXT_CONTENT)

    def get_setting(self, name):
        self.marionette.push_permission('settings-read', True)
        self.marionette.push_permission('settings-api-read', True)
        return self.marionette.execute_async_script(
            'return GaiaDataLayer.getSetting("%s")' % name)

    @property
    def all_settings(self):
        return self.get_setting('*')

    def set_setting(self, name, value):
        self.marionette.push_permission('settings-write', True)
        self.marionette.push_permission('settings-api-write', True)
        import json
        value = json.dumps(value)
        result = self.marionette.execute_async_script('return GaiaDataLayer.setSetting("%s", %s)' % (name, value))
        assert result, "Unable to change setting with name '%s' to '%s'" % (name, value)

    def _get_pref(self, datatype, name):
        self.marionette.switch_to_frame()
        with self.marionette.using_context('chrome'):
            pref = self.marionette.execute_script("return Services.prefs.get%sPref('%s');" % (datatype, name))
        return pref

    def _set_pref(self, datatype, name, value):
        value = json.dumps(value)
        self.marionette.switch_to_frame()
        with self.marionette.using_context('chrome'):
            self.marionette.execute_script(
                "Services.prefs.set%sPref('%s', %s);" % (datatype, name, value))

    def clear_user_pref(self, name):
        self.marionette.switch_to_frame()
        with self.marionette.using_context('chrome'):
            self.marionette.execute_script(
                "Services.prefs.clearUserPref('%s');" % name)

    def get_bool_pref(self, name):
        """Returns the value of a Gecko boolean pref, which is different from a Gaia setting."""
        return self._get_pref('Bool', name)

    def set_bool_pref(self, name, value):
        """Sets the value of a Gecko boolean pref, which is different from a Gaia setting."""
        return self._set_pref('Bool', name, value)

    def get_int_pref(self, name):
        """Returns the value of a Gecko integer pref, which is different from a Gaia setting."""
        return self._get_pref('Int', name)

    def set_int_pref(self, name, value):
        """Sets the value of a Gecko integer pref, which is different from a Gaia setting."""
        return self._set_pref('Int', name, value)

    def get_char_pref(self, name):
        """Returns the value of a Gecko string pref, which is different from a Gaia setting."""
        return self._get_pref('Char', name)

    def set_char_pref(self, name, value):
        """Sets the value of a Gecko string pref, which is different from a Gaia setting."""
        return self._set_pref('Char', name, value)

    def set_volume(self, value):
        channels = ['alarm', 'content', 'notification']
        for channel in channels:
            self.set_setting('audio.volume.%s' % channel, value)

    def bluetooth_enable(self):
        return self.marionette.execute_async_script("return GaiaDataLayer.enableBluetooth()")

    def bluetooth_disable(self):
        return self.marionette.execute_async_script("return GaiaDataLayer.disableBluetooth()")

    @property
    def bluetooth_is_enabled(self):
        return self.marionette.execute_script("return GaiaDataLayer.getBluetoothDefaultAdapter().state === 'enabled'")

    @property
    def bluetooth_is_discoverable(self):
        return self.marionette.execute_script("return GaiaDataLayer.getBluetoothDefaultAdapter().discoverable")

    @property
    def bluetooth_name(self):
        return self.marionette.execute_script("return GaiaDataLayer.getBluetoothDefaultAdapter().name")

    @property
    def is_cell_data_enabled(self):
        return self.get_setting('ril.data.enabled')

    def connect_to_cell_data(self):
        self.marionette.switch_to_frame()
        result = self.marionette.execute_async_script("return GaiaDataLayer.connectToCellData()")
        assert result, 'Unable to connect to cell data'

    def disable_cell_data(self):
        self.marionette.switch_to_frame()
        result = self.marionette.execute_async_script("return GaiaDataLayer.disableCellData()")
        assert result, 'Unable to disable cell data'

    @property
    def is_cell_data_connected(self):
        return self.marionette.execute_script('return window.navigator.mozMobileConnections && ' +
                                              'window.navigator.mozMobileConnections[0].data.connected;')

    def enable_cell_roaming(self):
        self.set_setting('ril.data.roaming_enabled', True)

    def disable_cell_roaming(self):
        self.set_setting('ril.data.roaming_enabled', False)

    @property
    def is_wifi_enabled(self):
        return self.marionette.execute_script("return window.navigator.mozWifiManager && "
                                              "window.navigator.mozWifiManager.enabled;")

    def enable_wifi(self):
        self.marionette.switch_to_frame()
        result = self.marionette.execute_async_script("return GaiaDataLayer.enableWiFi()")
        assert result, 'Unable to enable WiFi'

    def disable_wifi(self):
        self.marionette.switch_to_frame()
        result = self.marionette.execute_async_script("return GaiaDataLayer.disableWiFi()")
        assert result, 'Unable to disable WiFi'

    def connect_to_wifi(self, network=None):
        network = network or self.testvars.get('wifi')
        assert network, 'No WiFi network provided'
        self.enable_wifi()
        self.marionette.switch_to_frame()
        result = self.marionette.execute_async_script("return GaiaDataLayer.connectToWiFi(%s)" % json.dumps(network),
                                                      script_timeout=max(self.marionette.timeout, 60000))
        assert result, 'Unable to connect to WiFi network'

    def forget_all_networks(self):
        self.marionette.switch_to_frame()
        self.marionette.execute_async_script('return GaiaDataLayer.forgetAllNetworks()')

    def is_wifi_connected(self, network=None):
        network = network or self.testvars.get('wifi')
        self.marionette.switch_to_frame()
        return self.marionette.execute_script("return GaiaDataLayer.isWiFiConnected(%s)" % json.dumps(network))

    @property
    def known_networks(self):
        known_networks = self.marionette.execute_async_script(
            'return GaiaDataLayer.getKnownNetworks()')
        return [n for n in known_networks if n]

    @property
    def active_telephony_state(self):
        # Returns the state of only the currently active call or None if no active call
        return self.marionette.execute_script("return GaiaDataLayer.getMozTelephonyState()")

    @property
    def is_antenna_available(self):
        return self.marionette.execute_script('return window.navigator.mozFMRadio.antennaAvailable')

    @property
    def is_fm_radio_enabled(self):
        return self.marionette.execute_script('return window.navigator.mozFMRadio.enabled')

    @property
    def fm_radio_frequency(self):
        return self.marionette.execute_script('return window.navigator.mozFMRadio.frequency')

    @property
    def media_files(self):
        result = []
        result.extend(self.music_files)
        result.extend(self.picture_files)
        result.extend(self.video_files)
        return result

    def delete_all_sms(self):
        self.marionette.switch_to_frame()
        self.marionette.push_permission('sms', True)
        self.set_bool_pref('dom.sms.enabled', True)
        result = self.marionette.execute_async_script("return GaiaDataLayer.deleteAllSms();")
        self.marionette.push_permission('sms', False)
        self.clear_user_pref('dom.sms.enabled')
        return result

    def get_all_sms(self):
        self.marionette.switch_to_frame()
        self.marionette.push_permission('sms', True)
        self.set_bool_pref('dom.sms.enabled', True)
        result = self.marionette.execute_async_script("return GaiaDataLayer.getAllSms();")
        self.marionette.push_permission('sms', False)
        self.clear_user_pref('dom.sms.enabled')
        return result

    def delete_all_call_log_entries(self):
        """The call log needs to be open and focused in order for this to work."""
        self.marionette.execute_script('window.wrappedJSObject.RecentsDBManager.deleteAll();')

    def insert_call_entry(self, call):
        """The call log needs to be open and focused in order for this to work."""
        self.marionette.execute_script('window.wrappedJSObject.CallLogDBManager.add(%s);' % (json.dumps(call)))

        # TODO Replace with proper wait when possible
        import time
        time.sleep(1)

    def kill_active_call(self):
        self.marionette.execute_script("var telephony = window.navigator.mozTelephony; " +
                                       "if(telephony.active) telephony.active.hangUp();")

    def kill_conference_call(self):
        self.marionette.execute_script("""
        var callsToEnd = window.navigator.mozTelephony.conferenceGroup.calls;
        for (var i = (callsToEnd.length - 1); i >= 0; i--) {
            var call = callsToEnd[i];
            call.hangUp();
        }
        """)

    @property
    def music_files(self):
        return self.marionette.execute_async_script(
            'return GaiaDataLayer.getAllMusic();')

    @property
    def picture_files(self):
        return self.marionette.execute_async_script(
            'return GaiaDataLayer.getAllPictures();')

    @property
    def video_files(self):
        return self.marionette.execute_async_script(
            'return GaiaDataLayer.getAllVideos();')

    def sdcard_files(self, extension=''):
        files = self.marionette.execute_async_script(
            'return GaiaDataLayer.getAllSDCardFiles();')
        if len(extension):
            return [file for file in files if file['name'].endswith(extension)]
        return files

    def send_sms(self, number, message, skip_verification=False):
        self.marionette.switch_to_frame()
        import json
        number = json.dumps(number)
        message = json.dumps(message)

        self.marionette.push_permission('sms', True)
        self.set_bool_pref('dom.sms.enabled', True)
        result = self.marionette.execute_async_script('return GaiaDataLayer.sendSMS(%s, %s, %s)' % (number, message, str(skip_verification).lower()))
        self.marionette.push_permission('sms', False)
        self.clear_user_pref('dom.sms.enabled')

        assert result, 'Unable to send SMS to recipient %s with text %s' % (number, message)

    def add_notification(self, title, options=None):
        self.marionette.execute_script('new Notification("%s", %s);' % (title, json.dumps(options)))

    def clear_notifications(self):
        self.marionette.execute_script("window.wrappedJSObject.Service.request('NotificationScreen:clearAll');")

    @property
    def current_audio_channel(self):
        self.marionette.switch_to_frame()
        return self.marionette.execute_script("return window.wrappedJSObject.Service.query('currentChannel');")


class Accessibility(object):

    def __init__(self, marionette):
        self.marionette = marionette

    def is_hidden(self, element):
        return self._run_async_script('isHidden', [element])

    def is_visible(self, element):
        return self._run_async_script('isVisible', [element])

    def is_disabled(self, element):
        return self._run_async_script('isDisabled', [element])

    def click(self, element):
        self._run_async_script('click', [element])

    def wheel(self, element, direction):
        self.marionette.execute_script("""
        let element = arguments[0];
        let direction = arguments[1];
        let horizontal = direction === 'left' || direction === 'right';
        let page = (direction === 'left' || direction === 'up') ? 1 : -1;
        let event = new window.wrappedJSObject.WheelEvent('wheel', {
          bubbles: true,
          cancelable: true,
          deltaX: horizontal ? page : 0,
          deltaY: horizontal ? 0 : page,
          deltaMode: window.wrappedJSObject.WheelEvent.DOM_DELTA_PAGE,
        });
        element.wrappedJSObject.dispatchEvent(event);
        """, [element, direction], sandbox='default')

    def get_name(self, element):
        return self._run_async_script('getName', [element])

    def get_role(self, element):
        return self._run_async_script('getRole', [element])

    def dispatchEvent(self):
        self.marionette.switch_to_frame()
        self.marionette.execute_script(
            "window.dispatchEvent(new CustomEvent('accessibility-action'));")

    def execute_async_script(self, script, args, **kwargs):
        js = os.path.abspath(os.path.join(__file__, os.path.pardir,
                                          'atoms', "accessibility.js"))
        with open(js, 'r') as f:
            content = f.read()

        kwargs['sandbox'] = 'system'
        result = self.marionette.execute_async_script(
            '%s\n%s' % (content, script), args, **kwargs)

        if not result:
            return

        if result.has_key('error'):
            message = 'accessibility.js error: %s' % result['error']
            raise Exception(message)

        return result.get('result', None)

    def _run_async_script(self, func, args):
        return self.execute_async_script(
            'return Accessibility.%s.apply(Accessibility, arguments)' % func,
            args)


class GaiaDevice(object):

    def __init__(self, marionette, testvars=None, manager=None):
        self.manager = manager
        self.marionette = marionette
        self.testvars = testvars or {}

        if self.is_desktop_b2g:
            self.file_manager = GaiaLocalFileManager(self)
            # Use a temporary directory for storage
            self.storage_path = tempfile.mkdtemp()
            self._set_storage_path()
        elif self.manager:
            self.file_manager = GaiaDeviceFileManager(self)
            # Use the device root for storage
            self.storage_path = self.manager.deviceRoot

        self.lockscreen_atom = os.path.abspath(
            os.path.join(__file__, os.path.pardir, 'atoms', "gaia_lock_screen.js"))

    def _set_storage_path(self):
        if self.is_desktop_b2g:
            # Override the storage location for desktop B2G. This will only
            # work if the B2G instance is running locally.
            GaiaData(self.marionette).set_char_pref(
                'device.storage.overrideRootDir', self.storage_path)

    @property
    def is_android_build(self):
        if self.testvars.get('is_android_build') is None:
            self.testvars['is_android_build'] = 'boot2gecko' in self.marionette.session_capabilities['platformName'].lower()
        return self.testvars['is_android_build']

    @property
    def is_emulator(self):
        if not hasattr(self, '_is_emulator'):
            self._is_emulator = self.marionette.session_capabilities['device'] == 'qemu'
        return self._is_emulator

    @property
    def is_desktop_b2g(self):
        if self.testvars.get('is_desktop_b2g') is None:
            self.testvars['is_desktop_b2g'] = self.marionette.session_capabilities['device'] == 'desktop'
        return self.testvars['is_desktop_b2g']

    @property
    def is_online(self):
        # Returns true if the device has a network connection established (cell data, wifi, etc)
        return self.marionette.execute_script('return window.navigator.onLine;')

    @property
    def has_mobile_connection(self):
        return self.marionette.execute_script('return window.navigator.mozMobileConnections && ' +
                                              'window.navigator.mozMobileConnections[0].voice.network !== null')

    @property
    def has_wifi(self):
        if not hasattr(self, '_has_wifi'):
            self._has_wifi = self.marionette.execute_script('return window.navigator.mozWifiManager !== undefined')
        return self._has_wifi

    def restart_b2g(self):
        self.stop_b2g()
        time.sleep(2)
        self.start_b2g()

    def start_b2g(self, timeout=120):
        if self.marionette.instance:
            # launch the gecko instance attached to marionette
            self.marionette.instance.start()
        elif self.is_android_build:
            self.manager.shellCheckOutput(['start', 'b2g'])
        else:
            raise Exception('Unable to start B2G')
        self.marionette.wait_for_port()
        self.marionette.start_session()

        self.wait_for_b2g_ready(timeout)

        # Reset the storage path for desktop B2G
        self._set_storage_path()

    def wait_for_b2g_ready(self, timeout=120):
        # Wait for logo to be hidden
        self.marionette.set_search_timeout(0)
        try:
            Wait(self.marionette, timeout, ignored_exceptions=StaleElementException).until(
                lambda m: m.find_element(By.TAG_NAME, 'body').get_attribute('ready-state') == 'fullyLoaded')
        except NoSuchElementException:
            pass
        self.marionette.set_search_timeout(self.marionette.timeout or 10000)

    @property
    def is_b2g_running(self):
        return 'b2g' in self.manager.shellCheckOutput(['toolbox', 'ps'])

    def stop_b2g(self, timeout=5):
        if self.marionette.instance:
            # close the gecko instance attached to marionette
            self.marionette.instance.close()
        elif self.is_android_build:
            self.manager.shellCheckOutput(['stop', 'b2g'])
            Wait(self.marionette, timeout=timeout).until(
                lambda m: not self.is_b2g_running,
                message='b2g failed to stop.')
        else:
            raise Exception('Unable to stop B2G')
        self.marionette.client.close()
        self.marionette.session = None
        self.marionette.window = None

    def press_sleep_button(self):
        self.marionette.execute_script("""
            window.wrappedJSObject.dispatchEvent(new KeyboardEvent('mozbrowserbeforekeydown', {
              key: 'Power'
            }));""")

    def press_release_volume_up_then_down_n_times(self, n_times):
        self.marionette.execute_script("""
            function sendEvent(key, aType) {
              var type = aType === 'press' ? 'mozbrowserafterkeydown' : 'mozbrowserafterkeyup';
              window.wrappedJSObject.dispatchEvent(new KeyboardEvent(type, {
                key: key
              }));
            }
            for (var i = 0; i < arguments[0]; ++i) {
              sendEvent('VolumeUp', 'press');
              sendEvent('VolumeUp', 'release');
              sendEvent('VolumeDown', 'press');
              sendEvent('VolumeDown', 'release');
            };""", script_args=[n_times])

    def turn_screen_off(self):
        apps = GaiaApps(self.marionette)
        self.marionette.switch_to_frame()
        ret = self.marionette.execute_script("window.wrappedJSObject.Service.request('turnScreenOff', true)")
        apps.switch_to_displayed_app()
        return ret

    def turn_screen_on(self):
        apps = GaiaApps(self.marionette)
        self.marionette.switch_to_frame()
        ret = self.marionette.execute_script("window.wrappedJSObject.Service.request('turnScreenOn', true)")
        apps.switch_to_displayed_app()
        return ret

    @property
    def is_screen_enabled(self):
        apps = GaiaApps(self.marionette)
        self.marionette.switch_to_frame()
        ret = self.marionette.execute_script('return window.wrappedJSObject.Service.query("screenEnabled")')
        apps.switch_to_displayed_app()
        return ret

    def touch_home_button(self):
        from gaiatest.apps.homescreen.app import Homescreen
        homescreen = Homescreen(self.marionette)
        apps = GaiaApps(self.marionette)
        if homescreen.is_displayed == False:
            # touching home button will return to homescreen
            self._dispatch_home_button_event()
            homescreen.wait_to_be_displayed()
            apps.switch_to_displayed_app()
        else:
            self._dispatch_home_button_event()
            apps.switch_to_displayed_app()

            # touching home button inside homescreen will scroll it to the top
            Wait(self.marionette).until(lambda m: homescreen.is_at_topmost_position)

    def _dispatch_home_button_event(self):
        self.marionette.switch_to_frame()
        self.marionette.execute_script("window.wrappedJSObject.dispatchEvent(new Event('home'));")

    def hold_home_button(self):
        self.marionette.switch_to_frame()
        self.marionette.execute_script("window.wrappedJSObject.dispatchEvent(new Event('holdhome'));")
        # This is for the opacity animation to be finished for the task-manager
        # Otherwise we get intermittent issues tapping on opening a new browser window
        time.sleep(0.3)

    def hold_sleep_button(self):
        self.marionette.switch_to_frame()
        self.marionette.execute_script("window.wrappedJSObject.dispatchEvent(new Event('holdsleep'));")

    @property
    def is_locked(self):
        self.marionette.switch_to_frame()
        return self.marionette.execute_script("return window.wrappedJSObject.Service.query('locked')")

    def lock(self):
        self.marionette.switch_to_frame()
        GaiaData(self.marionette).set_setting('lockscreen.enabled', True)
        # Make sure the screen isn't turned off in lockscreen mode
        self.marionette.execute_script(
            'window.wrappedJSObject.ScreenManager.LOCKING_TIMEOUT = 9999;')
        self.turn_screen_off()
        self.turn_screen_on()
        assert self.is_locked, 'The screen is not locked'
        Wait(self.marionette).until(lambda m: m.find_element(By.CSS_SELECTOR, 'div.lockScreenWindow.active'))

    def unlock(self):
        if self.is_locked:
            self.marionette.import_script(self.lockscreen_atom)
            self.marionette.switch_to_frame()
            result = self.marionette.execute_async_script("GaiaLockScreen.unlock();", sandbox='default')
            GaiaData(self.marionette).set_setting('lockscreen.enabled', False)
            assert result, 'Unable to unlock screen'

    def change_orientation(self, orientation):
        """  There are 4 orientation states which the phone can be passed in:
        portrait-primary(which is the default orientation), landscape-primary, portrait-secondary and landscape-secondary
        """
        self.marionette.execute_async_script("""
            if (arguments[0] === arguments[1]) {
              marionetteScriptFinished();
            }
            else {
              var expected = arguments[1];
              window.screen.onmozorientationchange = function(e) {
                console.log("Received 'onmozorientationchange' event.");
                waitFor(
                  function() {
                    window.screen.onmozorientationchange = null;
                    marionetteScriptFinished();
                  },
                  function() {
                    return window.screen.mozOrientation === expected;
                  }
                );
              };
              console.log("Changing orientation to '" + arguments[1] + "'.");
              window.screen.mozLockOrientation(arguments[1]);
            };""", script_args=[self.screen_orientation, orientation])

    @property
    def screen_orientation(self):
        return self.marionette.execute_script('return window.screen.mozOrientation')


class GaiaTestCase(MarionetteTestCase, B2GTestCaseMixin):
    def __init__(self, *args, **kwargs):
        self.restart = kwargs.pop('restart', False)
        self.locale = kwargs.pop('locale')
        self.capture = kwargs.pop('capture')
        self.capturefolder = kwargs.pop('capturefolder')
        MarionetteTestCase.__init__(self, *args, **kwargs)
        B2GTestCaseMixin.__init__(self, *args, **kwargs)

    def setUp(self):
        try:
            MarionetteTestCase.setUp(self)
        except IOError:
            if self.restart:
                pass

        self.environment = GaiaTestEnvironment(self.testvars)
        self.device = GaiaDevice(self.marionette,
                                 manager=self.device_manager,
                                 testvars=self.testvars)

        if self.restart and (self.device.is_android_build or self.marionette.instance):
            # Restart if it's a device, or we have passed a binary instance with --binary command arg
            self.device.stop_b2g()
            try:
                if self.device.is_android_build:
                    self.cleanup_data()
                self.set_default_settings()
            finally:
                # make sure we restart to avoid leaving us in a bad state
                self.device.start_b2g()

        # We need to set the default timeouts because we may have a new session
        if self.marionette.timeout is None:
            # if no timeout is passed in, we detect the hardware type and set reasonable defaults
            timeouts = {}
            if self.device.is_desktop_b2g:
                self.marionette.timeout = 5000
                timeouts[self.marionette.TIMEOUT_SEARCH] = 5000
                timeouts[self.marionette.TIMEOUT_SCRIPT] = 10000
                timeouts[self.marionette.TIMEOUT_PAGE] = 10000
            elif self.device.is_emulator:
                self.marionette.timeout = 30000
                timeouts[self.marionette.TIMEOUT_SEARCH] = 30000
                timeouts[self.marionette.TIMEOUT_SCRIPT] = 60000
                timeouts[self.marionette.TIMEOUT_PAGE] = 60000
            else:
                # else, it is a device, the type of which is difficult to detect
                self.marionette.timeout = 10000
                timeouts[self.marionette.TIMEOUT_SEARCH] = 10000
                timeouts[self.marionette.TIMEOUT_SCRIPT] = 20000
                timeouts[self.marionette.TIMEOUT_PAGE] = 20000

            for k, v in timeouts.items():
                self.marionette.timeouts(k, v)

        else:
            # if the user has passed in --timeout then we override everything
            self.marionette.timeouts(self.marionette.TIMEOUT_SEARCH, self.marionette.timeout)
            self.marionette.timeouts(self.marionette.TIMEOUT_SCRIPT, self.marionette.timeout)
            self.marionette.timeouts(self.marionette.TIMEOUT_PAGE, self.marionette.timeout)

        self.apps = GaiaApps(self.marionette)
        self.data_layer = GaiaData(self.marionette, self.testvars)
        self.accessibility = Accessibility(self.marionette)

        self.cleanup_storage()

        if self.restart:
            self.cleanup_gaia(full_reset=False)
        else:
            self.cleanup_gaia(full_reset=True)

        if self.capture != "off":
            self.start_video_capture()

    # saves the captured video in /sdcard/ folder (only logical choice)
    # triggers screenrecord command as a thread since the command is blocking the main thread
    def start_video_capture(self):
        device_folder = "/sdcard"

        self.video_capture_filename = '%s_%s.mp4' \
                                      % (self.methodName, datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S'))

        def screen_record_command():
            self.device.manager.shellCheckOutput(cmd=['screenrecord',
                                                      os.path.join(device_folder, self.video_capture_filename)])

        if self.device.manager.dirExists(device_folder):
            thread.start_new_thread(screen_record_command, ())
        else:
            raise Exception('Unable to find internal storage folder')

    def stop_video_capture(self):
        # video file gets corrupted if using another signal
        self.device.manager.killProcess('screenrecord', sig=signal.SIGINT)
        # need to wait until the files are released, otherwise, files will be corrupted
        time.sleep(1)

    def pull_video_capture(self):
        # pull to the currently active directory
        self.device.manager.getFile(os.path.join("/sdcard", self.video_capture_filename),
                                    os.path.join(self.capturefolder, self.video_capture_filename))

    def cleanup_data(self):
        self.device.file_manager.remove('/cache/*')
        self.device.file_manager.remove('/data/b2g/mozilla')
        self.device.file_manager.remove('/data/local/debug_info_trigger')
        self.device.file_manager.remove('/data/local/indexedDB')
        self.device.file_manager.remove('/data/local/OfflineCache')
        self.device.file_manager.remove('/data/local/permissions.sqlite')
        self.device.file_manager.remove('/data/local/storage/permanent')
        self.device.file_manager.remove('/data/local/storage/persistent')
        self.device.file_manager.remove('/data/local/storage/default')
        # remove remembered networks
        self.device.file_manager.remove('/data/misc/wifi/wpa_supplicant.conf')

        if self.device.is_android_build:
            apps = json.loads(self.device.file_manager.pull_file('/data/local/webapps/webapps.json'))
            system_install_time = apps['system.gaiamobile.org']['installTime']
            for app in apps.values():
                if app.get('installTime') > system_install_time:
                    # removing any webapps installed since build time
                    path = posixpath.join(app.get('basePath'), app.get('id'))
                    self.logger.debug('Removing %s' % path)
                    self.device.file_manager.remove(path)

    def cleanup_storage(self):
        """Remove all files from the device's storage paths"""
        storage_paths = [self.device.storage_path]
        if self.device.is_android_build:
            # TODO: Remove hard-coded paths once bug 1018079 is resolved
            storage_paths.extend(['/mnt/sdcard/',
                                  '/mnt/extsdcard/',
                                  '/storage/sdcard/',
                                  '/storage/sdcard0/',
                                  '/storage/sdcard1/'])
        for path in storage_paths:
            if self.device.file_manager.dir_exists(path):
                for item in self.device.file_manager.list_items(path):
                    self.device.file_manager.remove('/'.join([path, item]))

    def cleanup_gaia(self, full_reset=True):

        self.device.turn_screen_off()
        self.device.turn_screen_on()

        # kill the FTU and any open, user-killable apps
        self.apps.kill_all()

        default_prefs = DEFAULT_PREFS.copy()
        default_prefs.update(self.testvars.get('prefs', {}))
        default_prefs = self.modify_prefs(default_prefs)
        for name, value in default_prefs.items():
            if type(value) is int:
                self.data_layer.set_int_pref(name, value)
            elif type(value) is bool:
                self.data_layer.set_bool_pref(name, value)
            else:
                self.data_layer.set_char_pref(name, value)

        # unlock
        if self.data_layer.get_setting('lockscreen.enabled'):
            self.device.unlock()

        if full_reset:
            default_settings = DEFAULT_SETTINGS.copy()
            default_settings.update(self.testvars.get('settings', {}))
            default_settings = self.modify_settings(default_settings)
            for name, value in default_settings.items():
                self.data_layer.set_setting(name, value)

            # disable carrier data connection
            if self.device.has_mobile_connection:
                self.data_layer.disable_cell_data()

            if self.device.has_wifi:
                # Bug 908553 - B2G Emulator: support wifi emulation
                if not self.device.is_emulator:
                    self.data_layer.enable_wifi()
                    self.data_layer.forget_all_networks()
                    self.data_layer.disable_wifi()

            # remove data
            self.data_layer.remove_all_contacts()

            # reset to home screen
            self.device.touch_home_button()

    def connect_to_local_area_network(self):
        if not self.device.is_online:
            if self.testvars.get('wifi') and self.device.has_wifi:
                self.data_layer.connect_to_wifi()
                assert self.device.is_online
            else:
                raise Exception('Unable to connect to local area network')

    def disable_all_network_connections(self):
        if self.device.has_wifi:
            self.data_layer.enable_wifi()
            self.data_layer.forget_all_networks()
            self.data_layer.disable_wifi()

        if self.device.has_mobile_connection:
            self.data_layer.disable_cell_data()

    def push_resource(self, filename, remote_path=None, count=1):
        # push to the test storage space defined by device root
        self.device.file_manager.push_file(
            self.resource(filename), remote_path, count)

    def resource(self, filename):
        return os.path.abspath(os.path.join(os.path.dirname(__file__), 'resources', filename))

    def modify_settings(self, settings):
        """Hook to modify the default settings before they're applied.

        :param settings: dictionary of the settings that would be applied.
        :returns: modified dictionary of the settings to be applied.

        This method provides the ability for test cases to override the default
        settings before they're applied. To use it, define the method in your
        test class and return a modified dictionary of settings:

        .. code-block:: python

            class TestModifySettings(GaiaTestCase):

                def modify_settings(self, settings):
                    settings['foo'] = 'bar'
                    return settings

                def test_modify_settings(self):
                    self.assertEqual('bar', self.data_layer.get_setting('foo'))

        """
        return settings

    def modify_prefs(self, prefs):
        """Hook to modify the default preferences before they're applied.

        :param prefs: dictionary of the preferences that would be applied.
        :returns: modified dictionary of the preferences to be applied.

        This method provides the ability for test cases to override the default
        preferences before they're applied. To use it, define the method in your
        test class and return a modified dictionary of preferences:

        .. code-block:: python

            class TestModifyPrefs(GaiaTestCase):

                def modify_prefs(self, prefs):
                    prefs['foo'] = 'bar'
                    return prefs

                def test_modify_prefs(self):
                    self.assertEqual('bar', self.data_layer.get_char_pref('foo'))

        """
        return prefs

    def set_default_settings(self):
        filename = 'settings.json'
        defaults = DEFAULT_SETTINGS.copy()
        defaults.update(self.testvars.get('settings', {}))
        defaults = self.modify_settings(defaults)

        if self.locale != 'undefined':
                defaults['language.current'] = self.locale

        if self.device.is_desktop_b2g:
            directory = self.marionette.instance.profile_path
            path = os.path.join(directory, filename)
        else:
            directory = '/system/b2g/defaults'
            path = posixpath.join(directory, filename)

        settings = json.loads(self.device.file_manager.pull_file(path))
        for name, value in defaults.items():
            self.logger.debug('Setting %s to %s' % (name, value))
            settings[name] = value
        td = tempfile.mkdtemp()
        try:
            tf = os.path.join(td, filename)
            with open(tf, 'w') as f:
                json.dump(settings, f)
            if not self.device.is_desktop_b2g:
                self.device.manager.remount()
            self.device.file_manager.push_file(tf, directory)
        finally:
            mozfile.remove(td)

    def wait_for_condition(self, method, timeout=None, message=None):
        Wait(self.marionette, timeout).until(method, message=message)

    @property
    def _has_thrown_any_exception_during_run(self):
        return sys.exc_info()[0] is not None

    def tearDown(self):
        self.marionette.switch_to_frame()
        if self.device.is_desktop_b2g and self.device.storage_path:
            shutil.rmtree(self.device.storage_path, ignore_errors=True)
        self.apps = None
        self.data_layer = None

        if self.capture != "off":
            self.stop_video_capture()
            # pull video file when there was an exception, or always set to pull
            if self.capture == "always" or \
                    (self.capture == "whenfail" and self._has_thrown_any_exception_during_run):
                self.pull_video_capture()

        MarionetteTestCase.tearDown(self)


class PasscodeTestCase(GaiaTestCase):

    def set_passcode_to_1337(self):
        """Set the passcode (but neither disable nor enable it)."""
        SET_DIGEST_VALUE = 'lockscreen.passcode-lock.digest.value'
        SET_DIGEST_SALT = 'lockscreen.passcode-lock.digest.salt'
        SET_DIGEST_ITERATIONS = 'lockscreen.passcode-lock.digest.iterations'
        SET_DIGEST_ALGORITHM = 'lockscreen.passcode-lock.digest.algorithm'

        settings = {}
        # The code for setting the passcode uses ArrayBuffers.
        # ArrayBuffers are represented as objects keys from 0 to n-1.
        # The settings DB does not support this and sees an array buffer of [3,6,9] objects
        # of the format {0: 3, 1: 6, 2: 9} (hence objects with keys from 0 to n-1)
        # n is array.length. So 8 for the salt and 20 for the digest.
        # The passcode is stored using PBKDF2 with a non-deterministic salt.
        # These values are the result of a pre-computation of PBKDF2 with the given salt,
        # 1000 iterations of SHA-1 and the passcode "1337".
        settings[SET_DIGEST_VALUE] = {"0": 119, "1": 161, "2": 123, "3": 75, "4": 210,
                                      "5": 67, "6": 1, "7": 189, "8": 48, "9": 33, "10": 242,
                                      "11": 167, "12": 140, "13": 241, "14": 255,
                                      "15": 39, "16": 5, "17": 23, "18": 43, "19": 150}
        settings[SET_DIGEST_SALT] = {"0": 89, "1": 203, "2": 232, "3": 38,
                                     "4": 249, "5": 94, "6": 109, "7": 54}
        settings[SET_DIGEST_ITERATIONS] = 1000
        settings[SET_DIGEST_ALGORITHM] = 'SHA-1'

        for setting, value in settings.iteritems():
            self.data_layer.set_setting(setting, value)
