How I asked my girlfriend the question… with a few lines of code

Stefan M.
5 min readMay 9, 2018

--

You may have noticed that I’ve asked my girlfriend the question. Well, I’m a Software Developer and enthusiastic in everything around programming. So I thought about writing an Android app that helps me to ask that simple question.

I’ve written a small app which can receive messages via Firebase Cloud Messaging. The app can handle these messages and react — based on the data content — on that.

Additionally to the Android client I created a backend server where I can create those messages dynamically.

Last but not least the frontend server. I’ve prepared some default messages which simply call the backend with predefined messages.

The App

I just followed the documentation to implement FCM and was ready to receive messages “in seconds”.

Because I wanted to decide how the app reacts on messages I decided to only support (and send) “data”-messages.

First, as FCM generates unique tokens for each registered device, I thought about using Firebase Realtime Database to store these tokens there and read it on the backend site while sending the message. But handling the token in that way was a little bit too over engineered. So I decided to subscribe the app to an “all” topic, meaning I could simply send the message to the topic “all” instead to a specific token.

class InstanceIdService : FirebaseInstanceIdService() {

override fun onTokenRefresh() {
super.onTokenRefresh()
FirebaseMessaging.getInstance().subscribeToTopic("all")
}

}

When a message was received on the device I parsed the JSON data content and put the content to a singleTop Activity.

class MessagingService : FirebaseMessagingService() {

override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)

remoteMessage.data?.apply {
val
sound = get(KEY_JSON_SOUND_KEY)
val image = get(KEY_JSON_IMAGE_KEY)
val text = get(KEY_JSON_TEXT_KEY)
val button = get(KEY_JSON_BUTTON_KEY)

startActivity(mainActivityIntent(
sound = sound,
image = image,
text = text,
button = button)
)
}
}
}

The Activity displayed the content (image, text and/or button) and played a sound — if the data provide one of these.

The backend send an image and a text

Don’t get scared about that UI. The custom font is expected and implemented with the support library. The text is blue because it is her favourite color. Because a white (or black) background looks a little bit boring (for that kind of app) I decided to use the contrast color of blue for that — which is obviously yellow.

The app has additional “tweaks” like no status- and no navigation-Bar. The Activity is not available in the recents and overrides the onBackPressed() (to don’t close the Activity on — well — back click).

Because my (ex- [😬]) girlfriend shouldn’t see the app on her phone I have removed the default launcher icon (and launcher Activity) on the release build type. With that config I was able to launch the Activity while developing via Android Studio (with the debug build type) but don’t on the release type.

// app/src/debug/AndroidManifest.xml
<application>
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
// app/src/release/AndroidManifest.xml
<application>
<activity
android:name=".MainActivity"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleTop"
android:screenOrientation="portrait"
/>
</application>

Have you noticed the android:exported="true" in the release AndroidManifest.xml?! That was needed to install the app on her phone “as fast as I can”. Well, I can’t say to her something like “Can I have your phone? I need it to install an app”. I have to install it “silently”. Furthermore the app has to be started once otherwise it doesn’t register it at FCM.

To achieve that I’ve done a little bit Gradle scripting. I just had to run the task installWaitAndClose while her device is plugged in and everything was ready to go.

tasks.create("install", Exec.class) {
commandLine "adb", "install", "-r", "$projectDir/build/outputs/apk/release/app-release.apk"
}

tasks.create("startActivity", Exec.class) {
commandLine "adb", "shell", "am", "start", "-n", "app.id/.MainActivity"
doLast {
sleep(10000)
}
}

tasks.create("killApp", Exec.class) {
commandLine "adb", "shell", "am", "force-stop app.id"
}

tasks.findByName("startActivity").dependsOn("install")
tasks.findByName("killApp").dependsOn("startActivity")
tasks.findByName("install").dependsOn("assembleRelease")

tasks.create("installWaitAndClose") {
dependsOn("install", "startActivity", "killApp")
}

The last mission was to grep her phone without her noticing…
Well, everyone does a nap time at the eve, right? 😉

The Backend

I created a small Python application which runs on Googles App Engine.
The app just reads the given QUERY, put it into a JSON and send it to the FCM server.

url = 'https://fcm.googleapis.com/fcm/send'
headers = {
'Content-Type' : 'application/json',
'Authorization' : '[SERVER_KEY]'
}
all_topic = "/topics/all"
data = json.dumps(
{
"to" : ""+ all_topic +"",
"data" : {
"sound" : "" + self.request.get("sound") + "",
"image" : "" + self.request.get("image") + "",
"text" : "" + self.request.get("text") + "",
"button" : "" + self.request.get("button") + ""
}
}
)
request = urllib2.Request(url, data, headers)
response = urllib2.urlopen(request)
html = response.read()
self.response.out.write(html)

Now I was able to call the following url in any browser I like which leads to a new message on the device were the Android app is installed:

[myBackend].appspot.com/mrge/
?text=I Love You
&sound=true
&image=https://i.imgur.com/H4HgbDu.jpg
&button=I love you too
A message with an image, text, button and sound (which is not visible of course)

The Frontend

Because I didn’t want to store the prepared urls in Googles Keep (for example) and open them one by one I’ve written a small HTML site and deployed it at now.sh. This simple website contains only a few buttons which will redirect a predefined url to the backend upon a button click.

<html>
<script type="text/javascript">
function messageOne() {
window.location.href = "[myBackend].appspot.com/mrge/?...
}
function messageTwo() {
window.location.href = "[myBackend].appspot.com/mrge/?...
}
</script>
<button onclick="messageOne()">First</button>
<button onclick="messageTwo()">Second</button>
</html>

I also — just for simplifying — moved that deployment to an custom alias.

At the time of asking I just had to grab my phone, open the website ([myCustomAlias].now.sh) and click the buttons — one by one at the given time.

Final Notes

Sending such a payload via FCM directly in the JSON data object is not recommended. Normally — in real life — you should just send a “messageId” or something like that. The app should then use this “messageId” to read the data from your server. For simplifying I’ve omitted this and sent it directly through FCM. Also note that the payload is limited.

Deploying both server sites (the backend & frontend) to different providers is probably not the best idea either. I could also provide the frontend over the App Engine or provide the backend via now.sh. I decided so only because of convenience.

Anyway. Seeing these three components
* an Android app written in Kotlin running on a phone
* a Python app running on Googles App Engine
* a HTML/JS app running on now.sh
working “hand in hand” is — from my point of view — really cool. Even if I develop software “professional” since 4+ years know.

I do something similar at work each day. But sometimes you have to just lean back and see in which amazing world we are living.

Oh and by the way: She said yes 🎉 👰🤵

--

--