Categories
Uncategorized

How to Implement Foreground Service and Deep Links for Android apps with calls? With Code Examples

Let’s take a look at 2 more UX conveniences for the Android caller application. First, let’s make sure that the app continues to function normally after minimizing or locking the screen with Android Foreground Services. After that, let’s see how we can implement direct links to a call or conference with Deep Links. By clicking on them, the smartphone users will be taken directly to the call.

How to create a Foreground Service on Android

Today’s smartphones and their operating systems have many built-in optimizations aimed at extending battery life. And mobile app developers need to keep in mind the potential actions the system can take on the app. 

A prime example is freeing up resources and closing apps that the user is not actively interacting with at the moment. In this case, the system considers only the app that is currently displayed on the user’s screen to be “actively used”. All other running applications can be closed at any time if the system does not have enough resources for the actively used one. Thanks to this, we can open an infinite number of applications and not explicitly close them — the system will close the old ones, and when we return to them, the application will run again.

In general, this mechanism is convenient and necessary on mobile devices. But we want to bypass this restriction so that the call is protected from sudden closure by the system. Fortunately, it is possible to “mark” a part of the application as actively used, even if it is not displayed anymore. To do this, we use the Foreground Service. Note that even this does not give full protection from the system — but it increases the “priority” of the application in the eyes of the system and also allows you to keep some objects in memory even if `Activity` is closed.

Add permission to run such Android services:

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

Let’s implement our service itself. In its simplest form it’s just a subclass Service, which has a link to our `CallManager` (so it won’t be cleaned up by garbage collector):

class OngoingCallService : Service() {

    @Inject
    lateinit var abstractCallManager: AbstractCallManager

    // Implementation of an abstract method; we won’t use Bind so just return null
    override fun onBind(intent: Intent): IBinder? = null

}

Service is an application component and, like Activity, must be specified in `AndroidManifest.xml`:

<service
    // Class name of our service
    android:name=".OngoingCallService"
    android:enabled="true"
    // This flag meant that other applications can’t run this service
    android:exported="false"
    // Declare a type of our service
    android:foregroundServiceType="microphone|camera|phoneCall" />

Our Android Foreground Service starts up a bit differently than regular services:

private fun startForegroundService() {
    val intent = Intent(this, OngoingCallService::class.java)
    ContextCompat.startForegroundService(this, intent)
}

On Android versions above 8, the Foreground Service must call the startForeground method within a few seconds, otherwise, the application is considered to be hung (ANR). It is necessary to pass a notification to this method because, for security reasons, the presence of such services should be visible to the user (if you do not know or have forgotten how to create notifications, you can refresh your memory in one of our previous articles about call notifications on Android):

val notification = getNotification()
startForeground(ONGOING_NOTIFICATION_ID, notification)

Everything that we wrote in the previous article about notifications applies to this notification — you can update it with the list of call participants, add buttons to it, or change its design completely. The only difference is that this notification will be `ongoing` by default and users won’t be able to “swipe” it.

On Android 13 and above POST_NOTIFICATIONS permission is required to display notifications. Declare it in the manifest:

<uses-permission android:name=”android.permission.POST_NOTIFICATIONS” />

You also need to request this permission at runtime, for example when entering the application. To learn more about requesting permissions, read the documentation.

If the user denies the notification permission, they still see notices related to foreground services in the Foreground Services (FGS) Task Manager but don’t see them in the notification drawer.

When the call is over – the service must be stopped, otherwise, the application can be completely closed only through the settings, which is very inconvenient for users. Our service is stopped in the same way as usual Android services:

private fun stopForegroundService() {
    val intent = Intent(this, OngoingCallService::class.java)
    stopService(intent)
}

Starting and stopping a service is very convenient to implement if CallManager has a reactive field to monitor the status of the call, for example:

abstractCallManager.isInCall
    .collect { if (it) startForegroundService() else stopForegroundService() }

This is the whole implementation of the service, which will allow to some extent protect our minimized application from being closed by the system.

Android Deep Links Tutorial

An extremely user-friendly feature that simplifies the growth of the user base of the app is the links to a certain place in the app. If the user doesn’t have the app, the link opens a page on Google Play. In the context of call apps, the most successful use case is the ability to share a link to a call / meeting / room. The user wants to talk to someone, throws the link to the person he’s talking to, that person downloads the app, and then gets right into the call — what could be more convenient?

The links themselves to a particular location in the application are supported by the system without any additional libraries. But in order for the link to “survive” the installation of the application, we need to ask for help from Firebase Dynamic Links.

Let’s concentrate on the implementation of links handling in the application and leave their creation to backend developers.

So, the Android deep links with code examples. First, let’s add the library:

dependencies {
    implementation 'com.google.firebase:firebase-dynamic-links:20.1.1'
}

To the user, deep links are ordinary links that he clicks on. But before opening a link in the browser, the system looks through the registry of applications and finds those that have declared that they handle links of this domain. If such an application is found – instead of opening in the browser, it launches the same application and the link is passed to it. If there is more than one such application – the system window will be shown with a list where the user can choose which application to open the link with. If you own the link domain, you can protect yourself from opening such links by other applications while yours is installed.

To declare the links that our app can handle, we need to add our `Activity` an intent-filter in `AndroidManifest.xml`:

<activity ...>
    <intent-filter>
        // These action and category notify the system that we can “display” the links
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        // Description of the link which we can handle. In this case these are the links starting from calls://forasoft.com
        <data
            android:host="forasoft.com"
            android:scheme="calls"/>
    </intent-filter>
</activity>

When the user clicks the Dynamic Link and installs the application (or clicks on the link having the app already installed), the Activity will launch which is indicated as this link’s handler. In this Activity, we can get the link this way:

Firebase.dynamicLinks
        .getDynamicLink(intent)
        .addOnSuccessListener(this) { data ->
            val deepLink: Uri? = data?.link
        }

When using regular deep links, the data becomes a bit simpler:

val deepLink = intent?.data

That’s all, now all we have left is getting the parameters that interest us from the link and carrying out the actions in your application that are necessary to connect to the call:

    val meetindId = deepLink?.getQueryParameter("meetingid")
        if (meetingId != null) abstractCallManager.joinMeeting(meetingId)

Conclusion

In the final article of our cycle “what each application with calls should have” we’ve gone through keeping our application alive after minimizing it and using the deep links as a convenient option for invitations to the call. Now you know all the mechanisms that make the user experience better not only inside the application but also at the system level.

Read other articles from this cycle:

What Every Android App With Calls Should Have

How to Make a Custom Call Notification on Android? With Code Examples

How to Make Picture-in-Picture Mode on Android With Code Examples

How to Implement Audio Output Switching During the Call on Android App?

Reach out to us to develop your app with calls, either for Android or other platforms 🙂

Categories
Uncategorized

Fora Soft’s CEO Nikolay Sapunov Interview to GoodFirms: Focusing on Narrow Specialization in Video and Multimedia Software

Interview-with-the-CEO
Interview with the CEO – Software development company

Incorporated in 2005, Fora Soft develops e-learning, telemedicine, and video surveillance software. They augment reality, launch Internet TV, identify objects on video, and do not build anything else.

What is Fora Soft: narrow specialization in video and multimedia software since the very beginning

From multimedia avatar-based text chats, they proceeded to video communication. They were pioneers there when a few accomplished videos were the future. Fora Soft developed the 1st video chat for the most comprehensive social network in Russia, vk.com – when even Facebook didn’t think of video chatting. The team was operating with Cirrus and Stratus technologies back then which are not in use currently. That chat named Webca was trendy, with more than 1 million users.

Fora Soft entered the international market in 2010 with a video review project for the Healthcare for a U.S. entrepreneur. Since then, they’ve been operating on all things multimedia. Imagine thousands of people utilizing your video program, and you can even protect a human’s life which is a fantastic feeling.

Fora Soft is a one-stop shop where people receive services of a complete software development cycle. Customers do not have to explore different contractors to develop their web, mobile, and desktop applications. The team helps customers with everything from graphic design to programming.

The GoodFirms team interacts with Nikolay Sapunov, the CEO of Fora Soft to know more about the company and its services. Nikolay mentions that “As a CEO, I’m accountable for strategic planning and coordinating all the departments. I also carry out all final job interviews before we send an offer”. 

How Fora Soft started

Talking about the company’s inception idea, Nikolay says that it started with a passion for computers and technologies and not a wish to earn. “As a child, I fantasized about a laptop and was delighted when I bought one from the first salary at a plant where I worked during my school years in the summer. Since that time, I started learning how to administrate it, then how to code”. 

Nikolay started as a .NET software developer and then started organizing project teams as a Project manager. “While still studying at university, my friend and I started offering software development services. In 2005 we developed the first multimedia chat – a cartoon world where you pick a character, walk, and text chat with whoever you meet. From that moment, we start counting Fora Soft history”.

In 2010 Fora Soft entered an international market with the first order from the US: they supported an entrepreneur transform his business online. He used to send interpreters to hospitals on foot when a patient did not speak English. There were a few dozens interpreters in one city in Wisconsin. Fora Soft developed a video chat for him, and he started employing interpreters from all over the world. Now his business has 740 delegates serving 670 doctor offices.

Fora Soft now: among the leaders in mobile and web development on Goodfirms

Fora Soft’s experts design apps for mobile devices by using extensive experience in video apps gained over 16 years. They ensure comprehensive compliance with iOS and Android guidelines so that your users will encounter the quality they expect.

Before plunging into the advancement of your app, business analysts and project managers will devise a detailed engineering plan. This ensures that your app will adhere to your exact needs, budget, and desired timeframe.

Thus, delivering the pixel-precise product customers envisioned and ensuring it aligns and complies with the highest standards of all the leading digital stores endows Fora Soft to lead as one of the preeminent mobile app development service providers at GoodFirms.

The review obtained at GoodFirms reflects the potential of developers at Fora Soft.

Nikolay mentions that the value of portraying business on the Internet is indisputable. Yet, the appearance itself doesn’t guarantee anything. We help to gain success with a clear plan, best practices, and the most advanced technology.

As a top web development company, Fora Soft has the ability to meet the customer needs of any industry. They are a multifaceted business solutions provider and are proud to serve clients from a vast range of industry verticals.

Thus, aligning your business goals with relevant technology, timeline, and budget endows Fora Soft to lead as one of the flourishing website development companies at GoodFirms.

The review given by Anastasia to GoodFirms proves the quality of service offerings rendered by Fora Soft.

In conclusion, Nikolay mentions that they embrace change. “In the industry in which we create a vital part, change occurs every day. We work with small and functional teams for each component of the processes, which react with an expedition to the changes happening in the industry”. 

Moreover, he mentions that Fora Soft trusts employees with insight into the organization’s direction and delegates them with the power to make decisions. Mistakes are necessary for this process. But unless you make mistakes, you never discover.

Lastly, Nikolas shares that Fora Soft’s vision is to become a synonym of video and multimedia software development worldwide. If someone thinks that a video or multimedia application is required, then Fora Soft should be the first name to come to mind.

Thus, having read the summarized narration from Nikolay’s interview, one can also go through the interview published at GoodFirms.

About GoodFirms

Washington, D.C.-based GoodFirms is a maverick B2B research and reviews firm that aligns its efforts in finding web development and mobile app development service agencies delivering unparalleled services to its clients. GoodFirms’ extensive research process ranks the companies, boosts their online reputation and helps service seekers pick the right technology partner that meets their business needs.

Categories
Uncategorized

How to Implement Audio Output Switching During the Call on Android App?

android-audio-output-tutorial
Automatically change your audio output on Android app

Seamless and timely switching between the sound output devices on Android is a feature that is usually taken for granted, but the lack of it (or problems with it) is very annoying. Today we will analyze how to implement such switching in Android ringtones, starting from the manual switching by the user to the automatic switching when headsets are connected. At the same time, let’s talk about pausing the rest of the audio system for the duration of the call. This implementation is suitable for almost all calling applications since it operates at the system level rather than the call engine level, e.g., WebRTC.

Audio output device management

    All management of Android sound output devices is implemented through the system’s `AudioManager`. To work with it you need to add permission to `AndroidManifest.xml`:

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

    First of all, when a call starts in our app, it is highly recommended to capture the audio focus — let the system know that the user is now communicating with someone, and it is best not to be distracted by sounds from other apps. For example, if the user was listening to music, but received a call and answered — the music will be paused for the duration of the call.

    There are two mechanisms of audio focus request — the old one is deprecated, and the new one is available since Android 8.0. We implement for all versions of the system:

// Receiving an AudioManager sample
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
// We need a "request" for the new approach. Let's generate it for versions >=8.0 and leave null for older ones
@RequiresApi(Build.VERSION_CODES.O)
private fun getAudioFocusRequest() =
   AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).build()


// Focus request
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    // Use the generated request
    audioManager.requestAudioFocus(getAudioFocusRequest())
} else {
    audioManager.requestAudioFocus(
        // Listener of receiving focus. Let's leave it empty for the sake of simpleness
        { },
        // Requesting a call focus
        AudioAttributes.CONTENT_TYPE_SPEECH,
        AudioManager.AUDIOFOCUS_GAIN
    )
}

    It is important to specify the most appropriate `ContentType` and `Usage` — based on these, the system determines which of the custom volume settings to use (media volume or ringer volume) and what to do with the other audio sources (mute, pause, or allow to run as before).

val savedAudioMode = audioManager.mode
val savedIsSpeakerOn = audioManager.isSpeakerphoneOn
val savedIsMicrophoneMuted = audioManager.isMicrophoneMute

        Great, we’ve got audio focus. It is highly recommended to save the original AudioManager settings right away before changing anything – this will allow us to restore it to its previous state when the call is over. You should agree that it would be very inconvenient if one application’s volume control would affect all the others

        Now we can start setting our defaults. It may depend on the type of call (usually audio calls are on “speakerphone” and video calls are on “speakerphone”), on the user settings in the application or just on the last used speakerphone. Our conditional app is a video app, so we’ll set up the speakerphone right away:

// Moving AudioManager to the "call" state
audioManager.mode = AudioSystem.MODE_IN_COMMUNICATION
// Enabling speakerphone
audioManager.isSpeakerphoneOn = true

 Great, we have applied the default settings. If the application design provides a button to toggle the speakerphone, we can now very easily implement its handling:

audioManager.isSpeakerphoneOn = !audioManager.isSpeakerphoneOn

Monitoring the connection of headphones

        We’ve learned how to implement hands-free switching, but what happens if you connect headphones? Nothing, because `audioManager.isSpeakerphoneOn` is still `true`! And the user, of course, expects that when headphones are plugged in, the sound will start playing through them. And vice versa — if we have a video call, then when we disconnect the headphones the sound should start playing through the speakerphone. 

        There is no way out, we have to monitor the connection of the headphones. Let me tell you right away, the connection of wired and Bluetooth headphones is tracked differently, so we have to implement two mechanisms at once. Let’s start with wired ones and put the logic in a separate class:

class HeadsetStateProvider(
    private val context: Context,
    private val audioManager: AudioManager
) {
    // The current state of wired headies; true means enabled
    val isHeadsetPlugged = MutableStateFlow(getHeadsetState())

    // Create BroadcastReceiver to track the headset connection and disconnection events
    private val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent) {
            if (intent.action == AudioManager.ACTION_HEADSET_PLUG) {
                when (intent.getIntExtra("state", -1)) {
                    // 0 -- the headset is offline, 1 -- the headset is online
                    0 -> isHeadsetPlugged.value = false
                    1 -> isHeadsetPlugged.value = true
                }
            }
        }
    }

    init {
        val filter = IntentFilter(Intent.ACTION_HEADSET_PLUG)
        // Register our BroadcastReceiver
        context.registerReceiver(receiver, filter)
    }

    // The method to receive a current headset state. It's used to initialize the starting point.
    fun getHeadsetState(): Boolean {
        val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
        return audioDevices.any {
            it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
                    || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET
        }
    }
}

   In our example, we use `StateFlow` to implement subscription to the connection state, but instead, we can implement, for example, `HeadsetStateProviderListener`

        Now just initialize this class and observe the `isHeadsetPlugged` field, turning the speaker on or off when it changes:

headsetStateProvider.isHeadsetPlugged
    // If the headset isn't on, speakerphone is.
    .onEach { audioManager.isSpeakerphoneOn = !it }
    .launchIn(someCoroutineScope)

Bluetooth headphones connection monitoring

            Now we implement the same monitoring mechanism for such Android sound output devices as Bluetooth headphones:

class BluetoothHeadsetStateProvider(
    private val context: Context,
 private val bluetoothManager: BluetoothManager
) {

    val isHeadsetConnected = MutableStateFlow(getHeadsetState())

    init {
        // Receive the adapter from BluetoothManager and install our ServiceListener
        bluetoothManager.adapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
            // This method will be used when the new device connects
            override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
                // Checking if it is the headset that's active
                if (profile == BluetoothProfile.HEADSET)
                    // Refreshing state
                    isHeadsetConnected.value = true
            }

            // This method will be used when the new device disconnects
            override fun onServiceDisconnected(profile: Int) 
                if (profile == BluetoothProfile.HEADSET)
                    isHeadsetConnected.value = false
            }
        // Enabling ServiceListener for headsets
        }, BluetoothProfile.HEADSET)
    }

    // The method of receiving the current state of the bluetooth headset. Only used to initialize the starting state
    private fun getHeadsetState(): Boolean {
        val adapter = bluetoothManager.adapter
        // Checking if there are active headsets  
        return adapter?.getProfileConnectionState(BluetoothProfile.HEADSET) == BluetoothProfile.STATE_CONNECTED
    }

}

Now we implement the same monitoring mechanism for Bluetooth headphones:

To work with Bluetooth, we need another permission. For Android 12 and above, you need to declare in the manifest file and request at runtime following permission:

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

    For devices with Android 11 and below, you need to declare in the manifest:

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

And now to automatically turn on the speakerphone when no headset is connected, and vice versa when a new headset is connected:

combine(headsetStateProvider.isHeadsetPlugged, bluetoothHeadsetStateProvider.isHeadsetPlugged) { connected, bluetoothConnected ->
    audioManager.isSpeakerphoneOn = !connected && !bluetoothConnected
}
    .launchIn(someCoroutineScope)

Tidying up after ourselves.

When the call is over, the audio focus is no longer useful to us and we have to get rid of it. Let’s restore the settings we saved at the beginning:

audioManager.mode = savedAudioMode
audioManager.isMicrophoneMute = savedIsMicrophoneMuted
audioManager.isSpeakerphoneOn = savedIsSpeakerOn

And now, actually, let’s give away the focus. Again, the implementation depends on the system version:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 
   audioManager.abandonAudioFocusRequest(getAudioFocusRequest()) 
} else { 
   // Let's leave it empty for simplicity 
   audioManager.abandonAudioFocus { } 
}

Limitations

In the app you can switch the sound output between three device types:

  • speaker
  • earpiece or wired
  • Bluetooth device

However you cannot switch between two Bluetooth devices. On Android 11 though, there’s now a feature to add the device switch to Notification. The switcher displays all available devices with the enabled volume control feature. So it will simply not show users the devices they can’t switch to from the one they’re currently using as an output.

To add the switcher, use the notif with the Notification.MediaStyle style with MediaSession connected to it:

val mediaSession = MediaSession(this, MEDIA_SESSION_TAG)
val style = Notification.MediaStyle()
.setMediaSession(mediaSession.sessionToken)
val notification = Notification.Builder(this, CHANNEL_ID)
.setStyle(style)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.build()

But how does Spotify have that quick and easy device switcher?

Our reader has noticed that Spotify does have that feature where you can switch between any devices you need. We cannot know for sure how they do that. But what we assume is that most likely Spotify implemented audio devices switching with MediaRouter API. It is used for seamless data exchange between two devices. 

To learn more about the audio device switcher and MediaRouter, check out this article in Android Developer’s blog and the Media Routing documentation.

Other articles about Android calls

How to Make Picture-in-Picture Mode on Android With Code Examples

WebRTC on Android

How to Make a Custom Call Notification on Android? With Code Examples

What Every Android App With Calls Should Have

Bottom line

        Great, here we have implemented the perfect UX of switching between Android sound output devices in our app. The main advantage of this approach is that it is almost independent of the specific implementation of calls: in any case, the played audio will be controlled by `AudioManager’, and we control exactly at its level!

Categories
Uncategorized

Naseem from MobyTap, ‘You guys know what you’re doing.’

Our copywriter Nikita talked to Naseem, the CEO & Founder of MobyTap, a video review platform. On MobyTap, businesses can show how much they love their customers, and how much their customers love them back. Naseem came to us in 2016, and after 5 years of cooperation, he definitely has something to tell us!

Software Development Reviews – MobyTap

Was Fora Soft your first choice?

Amazingly it was Vadim (the Sales Head at Fora Soft) who found me. I was on an online auction platform; I had lots of offers, and I chose my company. But when Vadim approached me, I was like, okay, let’s chat. His personality and everything he did was professional. I thought ‘even though we’ve done the contract with somebody else, why not.’ He asked me to give Fora Soft an opportunity, and I did. And that’s where the ball got rolling.

So, it was Vadim who swooped in and won you with his personality, right?

Yeah, he’s shown me some examples of the work you’ve done, I was really impressed with that.

Share any before and after working with us?

It was my 1st time developing an app. I’ve been in the recycling business for the past 15 years. So when the idea came to me, I thought recycling and video apps are worlds apart. But since my customers love what we do, that’s the easiest way to get feedback and reviews. And I thought, yeah, make an app. Sounds simple, but when you get into it, you realize that it’s not as simple.

Can you share any measurable figures, such as revenue, number of crashes, etc?

Not many crashes. It’s just been a continuous improvement. So all we’ve been doing is improving the app, making it better. And your team is fabulous. It was all about communication, and getting my message to you. You guys made life very easy, and that’s what the business is about. It took a couple of years but the job was well done, compared to the other company I had used. I literally had to cancel them and let you do the whole app.

Hands down, you guys know what you’re doing.

Those guys took a lot of time and messed up. And you guys clearly showed that you’re professional in what you do. It makes me very happy.

Thank you for your kind words. Were there any difficulties while working with us?

The only difficulty was the technology wasn’t there, so we were doing things that were 4-5 years ahead. Things that Google was catching up with. We got stuck where we needed to make it simpler for the users to input the domain name of any company in the world. If you’re doing a review, the app would find where you’re located and find the local business you’re reviewing. Nobody in the world had done that. And Google, just that year, had finished their SDK for Android. We were stuck for 4-5 months. How do we get the whole world’s domain names into the app? Then the idea came. Well, Google’s doing an SDK, could we use it? And then your team said, “We could do it”. Several months of work are saved.

You guys literally saved me a fortune by coming up with an idea of how to get the whole world’s domain names into the app.

And it works today! Because of you, guys.

Can you rate us in terms of professionalism, communication, and dedication?

Not even thinking about the score. It’s 10/10 straight up.

Do you have anything else to add?

The best thing with Fora Soft is aftercare.

You’ve raised the bar so high now, I expect it from every company.

The aftercare was so great, and the communication was amazing, it blew me away. Even in the UK here, in England, we don’t get that aftercare.

You guys looked after me and literally held my hand. Any issues that arose, you fixed them instantly, it’s like waving a magic wand. You don\t get that much in the tech world.

You’ve proved that you can find a good company online that will do what you want with your budget. If you got a small budget or a high budget, you’ve managed it perfectly. I think it’s been 5 years now.

So, if somebody needs a video app, I instantly think of you and pass them your number and details.

Thank you very much! Even though recycling and video apps are, as you said, worlds apart, we do wish you all the best.

Got a project idea of your own? Maybe, you’ve tried to make it come to fruition but are dissatisfied with the results? Contact us using the form on this website, and we’d be happy to review your case and offer the best solution.

Kindly follow us on Instagram as we share a lot of information regarding projects. You can also DM us if that’s your preferred method of communication!