Universal Android Apps

Joris Verbogt
Oct 9 2020
Posted in Engineering & Technology

Publish your app on both Google Play and Huawei AppGallery

With the advent of Huawei Mobile Services as a direct competitor for Google Play Services, it may be advantageous for your app to support both platforms with just one code base.

HMS Core Converter Tool

Huawei provides developers with a full set of developer tools to quickly integrate the various services from HMS Core into your app. They even went so far as to create a specific Converter Tool that allows you to grab the code of your existing app written for Google Play Services and convert it to an app that uses HMS Core. This Converter Tool also allows you to generate an app that will run on both platforms, and selects the appropriate dependencies at runtime, or create specific builds for both environments.

The Converter Tool allows you to analyse your app and determine which conversion steps can be done automatically and which ones need further attention. This of course all depends on how your current code base integrates Google Play Services.

Use your own Service Manager Factory class

If the Converter Tool is not what you want or if your app does not use Google Play Services yet, but needs to support both Google and Huawei, you can also write your own adapter that manages both services in your app.

Let's create an example where your app needs to be able to use Location Services from both Google Play and HMS.

Start by defining a ServiceManager interface that will allow your app to have a single point of entry when using either service:

public interface ServiceManager {

    int SERVICE_TYPE_GMS = 1;
    int SERVICE_TYPE_HMS = 2;

    String TRANSPORT_GMS = "GCM";
    String TRANSPORT_HMS = "HMS";

    int getServiceType();

    Boolean checkMobileServices();

    /**
     * Get or create a location manager, depending on availability
     * @return
     */
    @Nullable
    LocationManager getLocationManager();
}

This way you can add other services later by adding specific managers per feature. For now, this will only give you a LocationManager, for which you need to define its interface:

public interface LocationManager {

    int INITIAL_TRIGGER_ENTER = 1;
    int INITIAL_TRIGGER_EXIT = 2;
    int INITIAL_TRIGGER_DWELL = 4;

    /**
     * Enable location updates
     */
    void enableLocationUpdates();

    /**
     * Disable location updates
     */
    void disableLocationUpdates();

    /**
     * @return the last known location
     */
    @Nullable
    Location getLastKnownLocation();

    /**
     * Handle a location update, to be called from location receiver
     * @param location
     */
    void handleLocationUpdate(@NonNull Location location);
}

Now it's time to implement these interfaces for both platforms.

Google Play Services Service Manager

public class ServiceManagerImpl implements ServiceManager {

    private static final String TAG = ServiceManagerImpl.class.getSimpleName();

    private static final String LOCATION_MANAGER = "com.myapp.location.gms.LocationManagerImpl";

    private Context mContext;
    private LocationManager mLocationManager;
    private boolean mLocationManagerAvailable = true;


    public ServiceManagerImpl(@NonNull Context context) {
    	mContext = context;
        if (!checkMobileServices()) {
            throw new IllegalStateException("Google Play Services not available");
        }
    }

    @Override
	public int getServiceType() {
    	return SERVICE_TYPE_GMS;
	}

	@Override
	public Boolean checkMobileServices() {
		// Check that Google Play services is available
		return (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mContext) == ConnectionResult.SUCCESS);
	}

	@Override
	@Nullable
	public LocationManager getLocationManager() {
		if (!mLocationManagerAvailable) {
			return null;
		} else {
			if (mLocationManager == null) {
				mLocationManager = tryCreateLocationManager();
			}
			return mLocationManager;
		}
	}

	private LocationManager tryCreateLocationManager() {
		try {
			Class<?> klass = Class.forName(LOCATION_MANAGER);
			LocationManager manager =
					(LocationManager) klass.getConstructor(Context.class).newInstance(mContext);
			Log.d(TAG, String.format("Created %s", LOCATION_MANAGER));
			return manager;
		} catch (Throwable throwable) {
			Log.d(TAG, "Unable to create Google Play Location Manager", throwable);
			mLocationManagerAvailable = false;
			return null;
		}
	}
}

HMS Service Manager implementation

public class ServiceManagerImpl implements ServiceManager {

    private static final String TAG = ServiceManagerImpl.class.getSimpleName();
    private static final String LOCATION_MANAGER = "com.myapp.location.hms.LocationManagerImpl";

    private Context mContext;
    private LocationManager mLocationManager;
    private boolean mLocationManagerAvailable = true;

    public ServiceManagerImpl(@NonNull Context context) {
        mContext = context;
        if (!checkMobileServices()) {
            throw new IllegalStateException("HMS not available");
        }
    }

    @Override
    public int getServiceType() {
        return SERVICE_TYPE_HMS;
    }

    @Override
    public Boolean checkMobileServices() {
        return (HuaweiApiAvailability.getInstance().isHuaweiMobileServicesAvailable(mContext) == ConnectionResult.SUCCESS);
    }

    @Override
    public LocationManager getLocationManager() {
        if (!mLocationManagerAvailable) {
            return null;
        } else {
            if (mLocationManager == null) {
                mLocationManager = tryCreateLocationManager();
            }
            return mLocationManager;
        }
    }

    private LocationManager tryCreateLocationManager() {
        try {
            Class<?> klass = Class.forName(LOCATION_MANAGER);
            LocationManager manager =
                    (LocationManager) klass.getConstructor(Context.class).newInstance(mContext);
            Log.d(TAG, String.format("Created %s", LOCATION_MANAGER));
            return manager;
        } catch (Throwable throwable) {
            Log.d(TAG, "Unable to create HMS Location Manager", throwable);
            mLocationManagerAvailable = false;
            return null;
        }
    }
}

Google Play Location Manager implementation

public class LocationManagerImpl implements LocationManager {

    private static final String TAG = LocationManagerImpl.class.getSimpleName();

    private Context mContext;

    private FusedLocationProviderClient mFusedLocationProviderClient;
    private boolean mLocationUpdatesStarted;
    private PendingIntent mLocationPendingIntent;

    private Location mLastKnownLocation;
    private Location mLastUpdatedLocation;

    public LocationManagerImpl(@NonNull Context context) {
        mContext = context;
        // Setup location updates intent
        Intent locationIntent = new Intent(mContext, LocationReceiver.class);
        locationIntent.setAction(MyApp.INTENT_ACTION_LOCATION_UPDATED);
        mLocationPendingIntent = PendingIntent.getBroadcast(mContext, 0, locationIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_UPDATE_CURRENT);
    }

    @Override
    public void enableLocationUpdates() {
        if (mFusedLocationProviderClient == null) {
            Log.i(TAG, "Enabling location updates");
            Log.i(TAG, "start new fused location client");
            mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(mContext);
            Log.i(TAG, "start new geofencing client");
            mGeofencingClient = LocationServices.getGeofencingClient(mContext);
        }
        startLocationUpdates();
    }

    @Override
    public void disableLocationUpdates() {
        Log.i(TAG, "Disabling location updates");
        if (mLocationUpdatesStarted) {
            stopLocationUpdates();
        }
        mGeocoder = null;
    }

    @SuppressWarnings({"MissingPermission"})
    private void startLocationUpdates() {
        Log.i(TAG, "Starting location updates");
        if (mFusedLocationProviderClient != null && !mLocationUpdatesStarted) {
            Log.i(TAG, "Getting initial location");
            mFusedLocationProviderClient.getLastLocation().addOnSuccessListener(currentLocation -> {
                if (currentLocation != null) {
                    handleLocationUpdate(currentLocation);
                } else {
                    Log.w(TAG, "no location found yet");
                }
                LocationRequest locationRequest = LocationRequest.create();
                locationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
                mFusedLocationProviderClient.requestLocationUpdates(locationRequest, mLocationPendingIntent).addOnSuccessListener(aVoid -> {
                    Log.i(TAG, "location updates started");
                    mLocationUpdatesStarted = true;
                }).addOnFailureListener(e -> {
                    Log.w(TAG, "location updates could not be started: " + e.getMessage());
                });
            });
        }
    }

    private void stopLocationUpdates() {
        Log.i(TAG, "Stopping location updates");
        if (mFusedLocationProviderClient != null) {
            mFusedLocationProviderClient.removeLocationUpdates(mLocationPendingIntent);
        }
        mLocationUpdatesStarted = false;
    }

    @Override
    public Location getLastKnownLocation() {
        return mLastKnownLocation;
    }

    @Override
    public void handleLocationUpdate(Location location) {
        Log.i(TAG, "location update received, accuracy is " + location.getAccuracy());
        Log.d(TAG, "new location is " + location.getLatitude() + "," + location.getLongitude());
        if (location.getLatitude() > 90.0 || location.getLatitude() < -90.0 || location.getLongitude() > 180.0 || location.getLongitude() < -180.0) {
            Log.w(TAG, "received an invalid location " + location.getLatitude() + "," + location.getLongitude());
        } else {
            mLastKnownLocation = location;
        }
    }

}

public class LocationReceiver extends BroadcastReceiver {

    private static final String TAG = LocationReceiver.class.getSimpleName();

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        Log.d(TAG, "onReceive: " + action);
        if (action != null) {
            switch (action) {
                case MyApp.INTENT_ACTION_LOCATION_UPDATED:
                    if (LocationResult.hasResult(intent)) {
                        LocationResult result = LocationResult.extractResult(intent);
                        Location location = result.getLastLocation();
                        if (location != null) {
                            onLocationUpdateReceived(location);
                        }
                    }
                    break;
            }
        }
    }

    public void onLocationUpdateReceived(Location location) {
        ServiceManager serviceManager = ServiceManagerFactory.getServiceManager();
        if (serviceManager != null && serviceManager.getLocationManager() != null) {
            serviceManager.getLocationManager().handleLocationUpdate(location);
        }
    }
}

HMS Location Manager implementation

public class LocationManagerImpl implements LocationManager {

    private static final String TAG = LocationManagerImpl.class.getSimpleName();

    private Context mContext;

    private FusedLocationProviderClient mFusedLocationProviderClient;
    private boolean mLocationUpdatesStarted;
    private PendingIntent mLocationPendingIntent;

    private Location mLastKnownLocation;
    private Location mLastUpdatedLocation;

    @Keep
    public LocationManagerImpl(@NonNull Context context) {
        mContext = context;
        // Setup location updates intent
        Intent locationIntent = new Intent(mContext, LocationReceiver.class);
        locationIntent.setAction(MyApp.INTENT_ACTION_LOCATION_UPDATED);
        mLocationPendingIntent = PendingIntent.getBroadcast(mContext, 0, locationIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_UPDATE_CURRENT);
    }

    @Override
    public void enableLocationUpdates() {
        if (mFusedLocationProviderClient == null) {
            Log.i(TAG, "Enabling location updates");
            Log.i(TAG, "start new fused location client");
            mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(mContext);
        }
        startLocationUpdates();
    }

    @Override
    public void disableLocationUpdates() {
        Log.i(TAG, "Disabling location updates");
        if (mLocationUpdatesStarted) {
            stopLocationUpdates();
        }
        mGeocoder = null;
    }

    private void startLocationUpdates() {
        Log.i(TAG, "Starting location updates");
        if (mFusedLocationProviderClient != null && !mLocationUpdatesStarted) {
            Log.i(TAG, "Getting initial location");
            mFusedLocationProviderClient.getLastLocation().addOnSuccessListener(currentLocation -> {
                if (currentLocation != null) {
                    handleLocationUpdate(currentLocation);
                } else {
                    Log.w(TAG, "no location found yet");
                }
                LocationRequest locationRequest = LocationRequest.create();
                locationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
                mFusedLocationProviderClient.requestLocationUpdates(locationRequest, mLocationPendingIntent).addOnSuccessListener(aVoid -> {
                   Log.i(TAG, "location updates started");
                   mLocationUpdatesStarted = true;
                }).addOnFailureListener(e -> {
                    Log.w(TAG, "location updates could not be started: " + e.getMessage());
                });
            });
        }
    }

    private void stopLocationUpdates() {
        Log.i(TAG, "Stopping location updates");
        mLastKnownLocation = null;
        if (mFusedLocationProviderClient != null) {
            mFusedLocationProviderClient.removeLocationUpdates(mLocationPendingIntent);
        }
        mLocationUpdatesStarted = false;
    }

    @Override
    public Location getLastKnownLocation() {
        return mLastKnownLocation;
    }

    public Task<Location> getCurrentLocation() {
        if (mFusedLocationProviderClient != null) {
            return mFusedLocationProviderClient.getLastLocation();
        } else {
            return null;
        }
    }

    @Override
    public void handleLocationUpdate(@NonNull Location location) {
        Log.i(TAG, "location update received, accuracy is " + location.getAccuracy());
        Log.d(TAG, "new location is " + location.getLatitude() + "," + location.getLongitude());
        if (location.getLatitude() > 90.0 || location.getLatitude() < -90.0 || location.getLongitude() > 180.0 || location.getLongitude() < -180.0) {
            Log.w(TAG, "received an invalid location " + location.getLatitude() + "," + location.getLongitude());
        } else {
            mLastKnownLocation = location;
        }
    }
}

public class LocationReceiver extends BroadcastReceiver {

    private static final String TAG = LocationReceiver.class.getSimpleName();

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        Log.d(TAG, "onReceive: " + action);
        if (action != null) {
            switch (action) {
                case MyApp.INTENT_ACTION_LOCATION_UPDATED:
                    if (LocationResult.hasResult(intent)) {
                        LocationResult result = LocationResult.extractResult(intent);
                        Location location = result.getLastLocation();
                        if (location != null) {
                            onLocationUpdateReceived(location);
                        }
                    }
                    break;
            }
        }
    }

    public void onLocationUpdateReceived(Location location) {
        ServiceManager serviceManager = ServiceManagerFactory.getServiceManager();
        if (serviceManager != null && serviceManager.getLocationManager() != null) {
            serviceManager.getLocationManager().handleLocationUpdate(location);
        }
    }
}

Use a factory to create the correct Service Manager

Now every time your code needs access to (in this example) the Location Manager, it calls the Service Manager for the respective platform the app is running on. An easy way to do this is to create a factory that is a singleton:

public class ServiceManagerFactory {
    private static final String TAG = ServiceManagerFactory.class.getSimpleName();

    private static final String GMS_MANAGER = "com.myapp.service.gms.ServiceManagerImpl";
    private static final String HMS_MANAGER = "com.myapp.service.hms.ServiceManagerImpl";

    private static final Object lock = new Object();
    private static ServiceManager instance;

    /**
     * Singleton
     * @return singleton instance of ServiceManager
     */
    public static ServiceManager getServiceManager(@NonNull Context context) {
        synchronized (lock) {
            if (instance == null) {
                instance = createServiceManager(context);
            }
            return instance;
        }
    }

    public static ServiceManager createServiceManager(@NonNull Context context) {
        ServiceManager manager;
        manager = tryCreateHmsManager(context);
        if (manager == null) {
            Log.w(TAG, "HMS not found, trying Google Play Services");
            manager = tryCreateGmsManager(context);
        }
        if (manager == null) {
            Log.w(TAG, "no HMS or Google Play Services found");
        }
        return manager;
    }

    @Nullable
    private static ServiceManager tryCreateGmsManager(@NonNull Context context) {
        try {
            Class<?> klass = Class.forName(GMS_MANAGER);
            ServiceManager manager =
                    (ServiceManager) klass.getConstructor(Context.class).newInstance(context);
            Log.d(TAG, String.format("Created %s", GMS_MANAGER));
            return manager;
        } catch (Throwable throwable) {
            Log.d(TAG, "Unable to create Google Play Service Manager", throwable);
            return null;
        }
    }

    @Nullable
    private static ServiceManager tryCreateHmsManager(@NonNull Context context) {
        try {
            Class<?> klass = Class.forName(HMS_MANAGER);
            ServiceManager manager =
                    (ServiceManager) klass.getConstructor(Context.class).newInstance(context);
            Log.d(TAG, String.format("Created %s", HMS_MANAGER));
            return manager;
        } catch (Throwable throwable) {
            Log.d(TAG, "Unable to create HMS Service Manager", throwable);
            return null;
        }
    }
}

To get your Service Manager, or, for that matter, your Location Manager, just call the following from anywhere in your code:

ServiceManager sm = ServiceManagerFactory.getServiceManager(context);
if (sm != null) {
    LocationManager lm = sm.getLocationManager();
}

Conclusion

As you can see, there are several ways to integrate different service platforms in your app without the need for writing specific apps for both. Give the HMS Core Converter Tool a try or use the above code as an example to build your own abstraction layer.

If your app needs a ready-to-go integration of Push Notifications, Location Services, Beacons, Scannables and more, the Notificare SDK gives you the opportunity to tap into these features on both HMS and Google Play while unlocking all the power of our platform in your app.

If you are interested in using Notificare, or have any questions about cross-platform app development like described in this article, don't hesitate to contact us, as always, via our Support Channel.

It is also worthwhile to mention that if your app already has a substantial install base on Google Play, Huawei can offer you support in getting your app in the Huawei AppGallery quickly. Don't hesitate to contact us, if you want us to get you in touch with the Huawei team.

Keep up-to-date with the latest news