Back

Our open source FutuParty app

Daniel Landau • Software Developer

...or why diversity of ideas matters in a company

It’s 2017, so when Futurice organises a party, of course that party has an app. It’s called Futufinlandia after the name of this years FutuParty and it’s available at the Play Store and at the App Store. It’s kinda cool. It’s also something that’s not immediately useful for me with those details because I don’t have a device running either of those stores.

Wait, what?

I’m not a luddite, I do have a smartphone. It does run a version of Android, but there’s a slight difference. Google publishes the Android Open Source Project (AOSP) and Android With the Google Experience (not an official name). The latter is the thing you get when you buy an “Android phone” from the store. It’s also a thing I haven’t used for many years now. I have my reasons, personal freedom (from outside control, from surveillance), privacy, even performance. I know this is a choice not shared by many, in fact I think in a company of 400 employees I’m probably the only one who made this choice. I think in last year’s party there was one other among the guests.

If I don’t get my apps from the Play Store, how do I get them then, you might ask. I mainly use free and open source apps, and get most of them from F-Droid. While the Futufinlandia app is not in F-Droid, it is open source. It is good to stop here for a moment to appreciate how cool this is. Some companies would not share the source code of an app like this even with the employees, Futurice shares it with the world! Getting the app on my phone should be a quick matter of cloning, compiling and installing.

It almost is, but not quite. When writing Android apps, you rely on three things outside the code you write yourself: third party libraries, things provided by the Android system, and things provided by Google Play Services. I don’t run Google Play Services on my phone, so the last part is problematic.

At Futurice we value diversity in many ways and one aspect of that is diversity of ideas. While people who run Android without Google Play Services for open source reasons is, and will probably remain for a good time a market that is too small to matter for our business, there is another market that doesn’t run Google Play Services that could well be relevant for us in the future: China. The Chinese government and Google have a difficult relationship and whether or not Google services are available in China shifts from year to year, but the Android Open Source Project is used a lot in China with Chinese additional services. Coping with that is definitely something that can be learned quickly by competent developers, but a little bit of diversity in habits might give us a small edge of already collectively being somewhat familiar with how to make Android apps that don’t rely on Google.

How did our app end up depending on Google Play Services, you might ask. Thankfully not in many ways in this instance. We recycle the same base app (originally created for Whappu for the technical university students in Tampere) for all our parties and trips. On our trip to Prague, there was a map view in the app, and even though our declared dependency was an open source react native component, it uses Google Maps from Play Services under the hood. In all apps we use an open source library to get device info, and out of there we use a unique ID for analytics purposes. That library in turn again punts to Google Play Services for the unique ID. Even though our open source app isn’t directly dependent on anything that isn’t open source as well, these things creep up easily by accident.

These same open source dependencies are used in the apps we create for our clients as well. Now that we work mainly in western Europe where every Android device is sold with Google Play Services, it’s not an issue. It is a problem in China though, and even in the west Amazon tried to bring to market the Fire Phone that didn’t have Google Play Services.

The rest of this post is about technical details related to React Native and code, and it is not required reading unless one is interested in those things.

How to de-googlify a React Native app, step by step

  1. git clone https://github.com/futurice/event-app-client

  2. cd event-app-client && git checkout futuparty17

  3. cp env.example.js env.js

  4. Go to https://apk-dl.com/, input https://play.google.com/store/apps/details?id=com.futufinlandia

  5. extract with unzip

  6. vim assets/index.android.bundle

  7. grep for API_KEY

  8. Copy-paste values to env.js

  9. react-native run-android --variant=release

  10. npm install

  11. react-native run-android --variant=release

  12. yaourt android 23 pick android-platform-23 I’m running Arch, so I use system packages to install Android tools, they reside in /opt not in /home

  13. Get an error A problem occurred configuring project ':app'. \> A problem occurred configuring project ':react-native-device-info'. \> Could not resolve all dependencies for configuration ':react-native-device-info:_debugCompile'. \> Could not find any matches for com.google.android.gms:play-services-gcm:+ as no versions of com.google.android.gms:play-services-gcm are available.

  14. ag react-native-device-info

  15. remove references, in some places patch with result of python -c 'import uuid; print(uuid.uuid4())'

  16. react-native run-android --variant=release

  17. Get an error /home/dlan/local/src/event-app-client/android/app/src/main/java/com/futufinlandia/MainApplication.java:8: error: package com.learnium.RNDeviceInfo does not existimport com.learnium.RNDeviceInfo.RNDeviceInfo;

  18. Patch that out

  19. react-native run-android --variant=release

  20. Get an error /home/dlan/local/src/event-app-client/android/app/src/main/java/com/futufinlandia/MainActivity.java:10: error: package com.learnium.RNDeviceInfo does not existimport com.learnium.RNDeviceInfo.RNDeviceInfo;

  21. Patch that out

  22. react-native run-android --variant=release (copy-pasting keystore details is starting to get old)

  23. ???

  24. Profit!

Below is the final full diff of changes to the open source project.

    diff --git a/android/app/build.gradle b/android/app/build.gradle
    index dc67dc4..e34dec8 100644
    --- a/android/app/build.gradle
    +++ b/android/app/build.gradle
    @@ -152,7 +152,6 @@ dependencies {
    compile project(':react-native-linear-gradient')
    compile project(':react-native-image-zoom')
    compile project(':react-native-image-picker')
    - compile project(':react-native-device-info')
    compile project(':react-native-blur')
    compile fileTree(dir: "libs", include: ["*.jar"])
    compile 'com.facebook.fresco:animated-gif:0.12.0'
    diff --git a/android/app/src/main/java/com/futufinlandia/MainActivity.java b/android/app/src/main/java/com/futufinlandia/MainActivity.java
    index 6a01420..9d474ed 100644
    --- a/android/app/src/main/java/com/futufinlandia/MainActivity.java
    +++ b/android/app/src/main/java/com/futufinlandia/MainActivity.java
    @@ -7,7 +7,7 @@ import com.reactnative.photoview.PhotoViewPackage;
    import com.BV.LinearGradient.LinearGradientPackage;
    import com.image.zoom.ReactImageZoom;
    import com.imagepicker.ImagePickerPackage;
    -import com.learnium.RNDeviceInfo.RNDeviceInfo;
    +// import com.learnium.RNDeviceInfo.RNDeviceInfo;
    import com.cmcewen.blurview.BlurViewPackage;
    import com.image.zoom.ReactImageZoom;
    import com.imagepicker.ImagePickerPackage;
    diff --git a/android/app/src/main/java/com/futufinlandia/MainApplication.java b/android/app/src/main/java/com/futufinlandia/MainApplication.java
    index 21b33a0..28ff932 100644
    --- a/android/app/src/main/java/com/futufinlandia/MainApplication.java
    +++ b/android/app/src/main/java/com/futufinlandia/MainApplication.java
    @@ -5,7 +5,7 @@ import android.util.Log;
    
    import com.facebook.react.ReactApplication;
    import cl.json.RNSharePackage;
    -import com.learnium.RNDeviceInfo.RNDeviceInfo;
    +// import com.learnium.RNDeviceInfo.RNDeviceInfo;
    import com.facebook.react.ReactInstanceManager;
    import com.facebook.react.ReactNativeHost;
    import com.facebook.react.ReactPackage;
    @@ -31,7 +31,7 @@ public class MainApplication extends Application implements ReactApplication {
    @Override
    protected List<ReactPackage> getPackages() {
    return Arrays.<ReactPackage>asList(
    - new RNDeviceInfo(),
    + // new RNDeviceInfo(),
    new VectorIconsPackage(),
    new LinearGradientPackage(),
    new PhotoViewPackage(),
    diff --git a/android/settings.gradle b/android/settings.gradle
    index 45b3348..74ea30d 100644
    --- a/android/settings.gradle
    +++ b/android/settings.gradle
    @@ -13,7 +13,5 @@ include ':react-native-image-zoom'
    project(':react-native-image-zoom').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-zoom/android')
    include ':react-native-image-picker'
    project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android')
    -include ':react-native-device-info'
    -project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-device-info/android')
    include ':react-native-blur'
    project(':react-native-blur').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-blur/android')
    diff --git a/app/actions/registration.js b/app/actions/registration.js
    index ed9c1bb..e2b8104 100644
    --- a/app/actions/registration.js
    +++ b/app/actions/registration.js
    @@ -1,9 +1,13 @@
    -import DeviceInfo from 'react-native-device-info';
    +// import DeviceInfo from 'react-native-device-info';
    import api from '../services/api';
    import namegen from '../services/namegen';
    import _ from 'lodash';
    import {createRequestActionTypes} from './index';
    
    +const DeviceInfo = {
    + getUniqueID: () => '9797da75-e979-4f0e-a337-d37da32a8dbd'
    +}
    +
    const {
    CREATE_USER_REQUEST,
    CREATE_USER_SUCCESS,
    diff --git a/app/components/map/_EventMap.android.js b/app/components/map/_EventMap.android.js
    index 4c6212a..18dce0d 100644
    --- a/app/components/map/_EventMap.android.js
    +++ b/app/components/map/_EventMap.android.js
    @@ -27,11 +27,12 @@ import theme from '../../style/theme';
    import LoadingStates from '../../constants/LoadingStates';
    
    // Disable map on some devices
    -const DeviceInfo = require('react-native-device-info');
    -const Manufacturer = DeviceInfo.getManufacturer();
    -const OSVersion = DeviceInfo.getSystemVersion();
    -const disableMap = Platform.OS === 'android' && parseInt(OSVersion) >= 6 &&
    - (Manufacturer.indexOf('Sony') >= 0 || Manufacturer === 'OnePlus');
    +// const DeviceInfo = require('react-native-device-info');
    +// const Manufacturer = DeviceInfo.getManufacturer();
    +// const OSVersion = DeviceInfo.getSystemVersion();
    +// const disableMap = Platform.OS === 'android' && parseInt(OSVersion) >= 6 &&
    +// (Manufacturer.indexOf('Sony') >= 0 || Manufacturer === 'OnePlus');
    +const disableMap = false;
    
    const MARKER_IMAGES = {
    EVENT: require('../../../assets/marker.png'),
    diff --git a/app/services/analytics.js b/app/services/analytics.js
    index 3fd7b9a..e840df4 100644
    --- a/app/services/analytics.js
    +++ b/app/services/analytics.js
    @@ -3,19 +3,19 @@ import {
    Analytics,
    Hits as GAHits
    } from 'react-native-google-analytics';
    -import DeviceInfo from 'react-native-device-info';
    +// import DeviceInfo from 'react-native-device-info';
    import {version as VERSION_NUMBER} from '../../package.json';
    
    const GA_TRACKING_ID = 'UA-41493366-6';
    const APP_NAME = 'Wappuapp';
    -const CLIENT_ID = DeviceInfo.getUniqueID();
    +// const CLIENT_ID = DeviceInfo.getUniqueID();
    const INSTALLER_ID = 'futu.tammerforce.wappuapp.' + Platform.OS;
    
    -var ga = new Analytics(GA_TRACKING_ID, CLIENT_ID, 1, DeviceInfo.getUserAgent());
    +// var ga = new Analytics(GA_TRACKING_ID, CLIENT_ID, 1, DeviceInfo.getUserAgent());
    
    function viewOpened(viewName) {
    - var screenView = new GAHits.ScreenView(APP_NAME, viewName, VERSION_NUMBER, INSTALLER_ID);
    - ga.send(screenView);
    + // var screenView = new GAHits.ScreenView(APP_NAME, viewName, VERSION_NUMBER, INSTALLER_ID);
    + // ga.send(screenView);
    }
    
    export default {
    diff --git a/app/services/api.js b/app/services/api.js
    index 090f026..4fc8001 100644
    --- a/app/services/api.js
    +++ b/app/services/api.js
    @@ -1,4 +1,4 @@
    -import DeviceInfo from 'react-native-device-info';
    +// import DeviceInfo from 'react-native-device-info';
    import { AsyncStorage } from 'react-native';
    import { isEmpty } from 'lodash';
    
    @@ -6,6 +6,9 @@ import Endpoints from '../constants/Endpoints';
    import { version as VERSION_NUMBER } from '../../package.json';
    import * as ENV from '../../env';
    
    +const DeviceInfo = {
    + getUniqueID: () => '9797da75-e979-4f0e-a337-d37da32a8dbd'
    +}
    
    const USER_UUID = DeviceInfo.getUniqueID();
    const API_TOKEN = ENV.API_TOKEN;
    ```