Handle viSer 4.0

- The viSer proxy app is not required anymore.
- Set the built-in dialer as the default dialer.
- Update the app settings from a fresh export.

Issue: INFRA-225
Change-Id: I266bf682cb39b7118b57f05684f5e6ae5512b7f0
Depends-On: I55648872e2a87cdad22f82a29a17e7bb9824638d
diff --git a/deploy.py b/deploy.py
index d768583..b9b3ed0 100755
--- a/deploy.py
+++ b/deploy.py
@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
 
+import abc
 import contextlib
 import dataclasses
 import logging
@@ -16,8 +17,15 @@
 
 ADB_DEVICES_PATTERN = re.compile(r"^([a-z0-9-]+)\s+device$", flags=re.M)
 
+ANDROID_6_SDK = 23
+ANDROID_7_SDK = 24
 
-class HostCommandError(BaseException):
+
+class BaseError(Exception, abc.ABC):
+    """An abstract error."""
+
+
+class HostCommandError(BaseError):
     """An error happened while issuing a command on the host."""
 
     def __init__(self, command, error_message):
@@ -27,7 +35,7 @@
         super(HostCommandError, self).__init__(message)
 
 
-class DeviceCommandError(BaseException):
+class DeviceCommandError(BaseError):
     """An error happened while sending a command to a device."""
 
     def __init__(self, serial, command, error_message):
@@ -42,6 +50,14 @@
     """An intent was rejected by a device."""
 
 
+class UnlicensedDeviceError(BaseError):
+    """A device is missing a license."""
+
+    def __init__(self, device, provider):
+        self.device = device
+        super().__init__(f"{device} is missing a license for {provider}")
+
+
 def adb(*args, serial=None, raise_on_error=True):
     """Run ADB command attached to serial.
 
@@ -164,6 +180,9 @@
         # Cache the OS flavour
         self.os_flavour = "gms" if self.is_gms_device() else "sibon"
 
+    def __str__(self) -> str:
+        return f"{DeviceUnderTest.__name__}({self.serial})"
+
     def adb(self, *args) -> subprocess.CompletedProcess:
         """Execute an adb command on this device.
 
@@ -256,7 +275,7 @@
         :raise DeviceCommandError: If the install command failed.
         """
         command = ["install", "-r"]
-        if self.sdk >= 23:
+        if self.sdk >= ANDROID_6_SDK:
             # From Marshmallow onwards, adb has a flag to grant default permissions
             command.append("-g")
 
@@ -369,32 +388,6 @@
 
 
 @dataclasses.dataclass(frozen=True)
-class ViserProxyApp:
-    """A template for the viSer Proxy app."""
-
-    package: str
-    """The app package name."""
-    apk_filename_template: str
-    """The string template of the APK filename, in the `str.format()` style (`{}`).
-
-    The following place-holders can be used and will be replaced at runtime:
-    - `{sdk}`: The Android SDK number, e.g. `25` for Android 7.1.
-    - `{flavour}`: The Fairphone-specific Android build flavour, e.g. `gms`
-      (Fairphone OS) or `sibon` (Fairphone Open).
-    """
-
-    def resolve(self, *, sdk: int, flavour: str) -> AndroidApp:
-        """Resolve the app template into a Viser App."""
-        if sdk >= 24:
-            sdk = 24
-        else:
-            sdk = 19
-        return AndroidApp(
-            self.package, self.apk_filename_template.format(sdk=sdk, flavour=flavour)
-        )
-
-
-@dataclasses.dataclass(frozen=True)
 class Credentials:
     """Credentials to access a service."""
 
@@ -414,10 +407,6 @@
 
     """
 
-    VISER_PROXY_APP_TEMPLATE = ViserProxyApp(
-        "com.lunarlabs.panda.proxy",
-        "com.lunarlabs.panda.proxy-latest-sdk{sdk}-{flavour}.apk",
-    )
     ANDROID_APPS = [
         AndroidApp("com.smartviser.demogame", "com.smartviser.demogame-latest.apk"),
         AndroidApp("com.lunarlabs.panda", "com.lunarlabs.panda-latest.apk"),
@@ -430,6 +419,9 @@
     settings_file: pathlib.Path
     target_path: pathlib.Path = pathlib.Path("/sdcard/Viser")
 
+    def __str__(self) -> str:
+        return "SmartViser viSer app suite"
+
     @property
     def target_scenarios_path(self) -> pathlib.Path:
         return self.target_path
@@ -450,11 +442,7 @@
             The sequence is ordered to satisfy the dependency graph
             (i.e. required apps come first in the sequence).
         """
-        return [
-            self.VISER_PROXY_APP_TEMPLATE.resolve(
-                sdk=device.sdk, flavour=device.os_flavour
-            )
-        ] + self.ANDROID_APPS
+        return self.ANDROID_APPS
 
     def deploy(self, device: DeviceUnderTest) -> None:
         """Deploy the suite on a device.
@@ -515,6 +503,8 @@
             device.install(self.prebuilts_path / app.apk)
 
         with device.launch("com.lunarlabs.panda"):
+            _LOG.info("Initiate first run of viSer")
+
             # Input the credentials
             device.ui(resourceId="android:id/content").child(text="Username").child(
                 className="android.widget.EditText"
@@ -524,7 +514,7 @@
             ).set_text(self.credentials.password)
 
             # Sign in
-            signin_label = "SIGN IN" if device.sdk >= 24 else "Sign in"
+            signin_label = "SIGN IN" if device.sdk >= ANDROID_7_SDK else "Sign in"
             device.ui(resourceId="android:id/content").child(
                 text=signin_label, className="android.widget.Button"
             ).click()
@@ -535,6 +525,15 @@
             )
             progress_bar.wait.gone(timeout=10000)
 
+            # Is the app licensed?
+            license_failed_info = device.ui(resourceId="android:id/content").child(
+                textContains="Licence verification failed"
+            )
+            if license_failed_info.exists:
+                raise UnlicensedDeviceError(device, self)
+
+            self._maybe_accept_viser_dialer(device)
+
     def configure_suite(self, device: DeviceUnderTest) -> None:
         """Configure the suite on a device.
 
@@ -546,11 +545,14 @@
         device.push(self.settings_file, self.target_settings_file)
 
         with device.launch("com.lunarlabs.panda"):
-            device.ui(text="Settings", className="android.widget.TextView").click()
-            device.ui(resourceId="android:id/list").child_by_text(
+            device.ui(description="Open navigation drawer").click()
+            device.ui(
+                text="Settings", className="android.widget.CheckedTextView"
+            ).click()
+            device.ui(className="android.support.v7.widget.RecyclerView").child_by_text(
                 "Settings management", className="android.widget.LinearLayout"
             ).click()
-            device.ui(resourceId="android:id/list").child_by_text(
+            device.ui(className="android.support.v7.widget.RecyclerView").child_by_text(
                 "Load settings", className="android.widget.LinearLayout"
             ).click()
             device.ui(resourceId="android:id/list").child_by_text(
@@ -560,6 +562,56 @@
                 textMatches="(?i)Select", className="android.widget.Button"
             ).click()
 
+            self._maybe_accept_viser_dialer(device)
+
+    def _maybe_accept_viser_dialer(self, device: DeviceUnderTest) -> None:
+        """Accept the built-in viSer dialer as the default dialer, if necessary.
+
+        This method tries to accept the built-in viSer dialer from a
+        currently displayed modal. Two types of modals are handled, the
+        modal spawned by the viSer app itself, and the modal spawned by
+        the system UI.
+
+        This method is idempotent and will pass even if the modals are
+        missing.
+
+        :param device: The device to accept the modal on.
+        """
+        dialer_set = False
+
+        # Optional modal to set the the built-in dialer as default
+        set_dialer_modal = device.ui(resourceId="android:id/content").child(
+            textContains="Do you want to use viSer dialer as default ?"
+        )
+        if set_dialer_modal.exists:
+            device.ui(resourceId="android:id/content").child(
+                textMatches="(?i)Yes", className="android.widget.Button"
+            ).click()
+            dialer_set = True
+
+        # Optional system modal to really set the built-in dialer as default
+        set_dialer_system_modal = device.ui(resourceId="android:id/content").child(
+            text="Make viSer your default Phone app?"
+        )
+        if set_dialer_system_modal.exists:
+            device.ui(resourceId="android:id/content").child(
+                textMatches="(?i)Set default", className="android.widget.Button"
+            ).click()
+            dialer_set = True
+
+        # Optional system modal to change the default dialer
+        change_dialer_system_modal = device.ui(resourceId="android:id/content").child(
+            text="Change default Dialer app?"
+        )
+        if change_dialer_system_modal.exists:
+            device.ui(resourceId="android:id/content").child(
+                textMatches="(?i)OK", className="android.widget.Button"
+            ).click()
+            dialer_set = True
+
+        if dialer_set:
+            _LOG.info("Set the built-in viSer dialer as default dialer")
+
 
 def deploy():
     serials = []
@@ -586,12 +638,12 @@
             device.unlock()
 
             # Disable Privacy Impact popup on Android 5.
-            if device.sdk <= 22:
+            if device.sdk < ANDROID_6_SDK:
                 disable_privacy_impact_popup(device)
 
             # Push the scenarios, their data, and install the apps
             suite.deploy(device)
-        except (HostCommandError, DeviceCommandError, uiautomator.JsonRPCError):
+        except (BaseError, uiautomator.JsonRPCError):
             _LOG.error("Failed to execute deployment", exc_info=True)
         finally:
             try:
diff --git a/settings/fairphone-bot_settings b/settings/fairphone-bot_settings
index f3f0aad..9f69433 100644
--- a/settings/fairphone-bot_settings
+++ b/settings/fairphone-bot_settings
@@ -1 +1 @@
-{"mAfterDefinedTime":false,"mAtDefinedTime":false,"mAutoAnswer":"disabled","mAutoAnswerDelay":"10","mAutoAnswerMax":"20","mAutoAnswerMin":"10","mAutoAnswerMode":false,"mAutoAnswerModeInject":true,"mAutoAnswerModeNative":false,"mAutoAnswerModeTap":false,"mAutoAnswerXCoordinate":"550","mAutoAnswerYCoordinate":"188","mBatteryEachUa":false,"mBatteryMonitoring":false,"mBatterySampling":false,"mBatterySamplingDuration":"60","mBatteryStatusChange":true,"mBuildScenarioMirror":false,"mBuildScenarioResults":true,"mBuildXmlReport":true,"mCallState":true,"mCampaignTitle":"","mCellLocation":true,"mCellNeighborhood":false,"mCellularSampling":true,"mCellularSamplingDuration":"10","mComparisonBatterySign":"1","mComparisonBatteryValue":"","mContinueIterateOnError":true,"mContinueLastCamp":false,"mContinueTestSuiteOnError":true,"mDataConnection":true,"mDataStall":true,"mDataStallInterval":"1000","mDefinedHours":"","mDefinedMinutes":"","mDefinedSeconds":"","mEmailAccount":"fairphone.viser@gmail.com","mEmailTo":"","mEnvironmentSensors":false,"mFilePattern":":date:-:time:_:name:","mFtpServerFolderPath":"","mFtpServerLogin":"","mFtpServerPort":"21","mFtpServerPwd":"","mFtpServerTo":"","mHardwareVersion":"","mInfiniteCampaign":false,"mLimitedBattery":false,"mLimitedDays":"","mLimitedHours":"","mLimitedMinutes":"","mLimitedRuns":false,"mLimitedTimes":false,"mLogIPChange":true,"mLogSignalsStrengths":true,"mNmeaLog":false,"mNumberRuns":"","mPhoneLocDistance":"5","mPhoneLocDuration":"5","mPhoneLocation":true,"mPrintAppsIcon":false,"mProductName":"","mQuestionFormPath":"","mReportApps":false,"mReportCpuUse":false,"mReportFTP":false,"mReportFTPCltsOnly":false,"mReportGroupSimilarTestEvents":true,"mReportMail":false,"mReportMemoryUse":false,"mReportSourceCode":false,"mReportTestEvents":true,"mRunMultiScenarioMethod":"ordered","mRunNoCpuWakeLock":false,"mSMSReport":false,"mSamplingCellInfo":false,"mSamplingDurationCPU":"250","mSamplingDurationCi":"1","mSamplingDurationPs":"2","mSamplingPs":false,"mSendMmsThroughSystem":false,"mServerAddress":"data.smartviser-data.com","mServiceState":true,"mSetRunExecScreenDarkTheme":false,"mSetRunListCollaped":false,"mShowFooter":true,"mShowReceivedMmsSms":false,"mShutdownAfterCompletion":false,"mSmsNumber":"","mStartBatteryLevel":false,"mStartBatterySign":"1","mStartBatteryValue":"","mTrackAPN":false,"mTrackHardwareVersion":true,"mTrackIMSI":true,"mTrackIPStatus":false,"mTrackImei":true,"mTrackLanguage":false,"mTrackModemVersion":true,"mTrackMsisdn":false,"mTrackNetworkName":false,"mTrackNetworkType":true,"mTrackProductName":true,"mTrackRoamingStatus":false,"mTrackSignalsStrengths":false,"mTrackSoftwareVersion":true,"mUDSamplingInterval":"2000","mUDSamplingUnit":"Mbps","mUplinkDownlink":true,"mUserMailPwd":"fairphoneviser2017","mVoltageMonitoring2":false,"mVwsSendCallstoServer":false,"mVwsSendOFilestoServer":false,"mVwsSendtoServer":true,"mWifiSampling":true,"mWifiSamplingDuration":"10"}
+{"mAfterDefinedTime":false,"mAtDefinedTime":false,"mAutoAnswer":"disabled","mAutoAnswerDelay":"10","mAutoAnswerMax":"20","mAutoAnswerMin":"10","mAutoAnswerMode":false,"mAutoAnswerModeInject":true,"mAutoAnswerModeNative":false,"mAutoAnswerModeTap":false,"mAutoAnswerXCoordinate":"550","mAutoAnswerYCoordinate":"188","mBatteryEachUa":false,"mBatteryMonitoring":false,"mBatterySampling":false,"mBatterySamplingDuration":"60","mBatteryStatusChange":true,"mBuildScenarioMirror":false,"mBuildScenarioResults":true,"mBuildXmlReport":true,"mCallState":true,"mCampaignTitle":"","mCellLocation":true,"mCellNeighborhood":false,"mCellularSampling":true,"mCellularSamplingDuration":"10","mComparisonBatterySign":"1","mComparisonBatteryValue":"","mContinueIterateOnError":true,"mContinueLastCamp":false,"mContinueTestSuiteOnError":true,"mDataConnection":true,"mDataStall":true,"mDataStallInterval":"1000","mDefinedHours":"","mDefinedMinutes":"","mDefinedSeconds":"","mEmailAccount":"fairphone.viser@gmail.com","mEmailTo":"","mEnvironmentSensors":false,"mExhaustedBattery":false,"mFilePattern":":date:-:time:_:name:","mFtpServerFolderPath":"","mFtpServerLogin":"","mFtpServerPort":"21","mFtpServerPwd":"","mFtpServerTo":"","mHardwareVersion":"","mInfiniteCampaign":false,"mLimitedBattery":false,"mLimitedDays":"","mLimitedHours":"","mLimitedMinutes":"","mLimitedRuns":false,"mLimitedTimes":false,"mLogIPChange":true,"mLogSignalsStrengths":true,"mNmeaLog":false,"mNumberRuns":"","mPhoneDialerApp":true,"mPhoneLocDistance":"5","mPhoneLocDuration":"5","mPhoneLocation":true,"mPrintAppsIcon":false,"mProductName":"","mQuestionFormPath":"","mReportApps":false,"mReportCpuUse":false,"mReportFTP":false,"mReportFTPCltsOnly":false,"mReportGroupSimilarTestEvents":true,"mReportMail":false,"mReportMemoryUse":false,"mReportSourceCode":false,"mReportTestEvents":true,"mRunMultiScenarioMethod":"ordered","mRunNoCpuWakeLock":false,"mSMSReport":false,"mSamplingCellInfo":false,"mSamplingDurationCPU":"250","mSamplingDurationCi":"1","mSamplingDurationPs":"2","mSamplingPs":false,"mSendMmsThroughSystem":false,"mServerAddress":"data.smartviser-data.com","mServiceState":true,"mSetRunExecScreenDarkTheme":false,"mSetRunListCollaped":false,"mShowFooter":true,"mShowReceivedMmsSms":false,"mShutdownAfterCompletion":false,"mSmsNumber":"","mStartBatteryLevel":false,"mStartBatterySign":"1","mStartBatteryValue":"","mTrackAPN":false,"mTrackHardwareVersion":true,"mTrackIMSI":true,"mTrackIPStatus":false,"mTrackImei":true,"mTrackLanguage":false,"mTrackModemVersion":true,"mTrackMsisdn":false,"mTrackNetworkName":false,"mTrackNetworkType":true,"mTrackProductName":true,"mTrackRoamingStatus":false,"mTrackSignalsStrengths":false,"mTrackSoftwareVersion":true,"mUDSamplingInterval":"2000","mUDSamplingUnit":"Mbps","mUplinkDownlink":true,"mUserMailPwd":"fairphoneviser2017","mVoltageMonitoring2":false,"mVwsSendCallstoServer":false,"mVwsSendOFilestoServer":false,"mVwsSendtoServer":true,"mWifiSampling":true,"mWifiSamplingDuration":"10"}