Skip to content

Instantly share code, notes, and snippets.

@binouze
Last active June 2, 2023 03:26
Show Gist options
  • Save binouze/a574eee22e08b6ec076cfe594897f191 to your computer and use it in GitHub Desktop.
Save binouze/a574eee22e08b6ec076cfe594897f191 to your computer and use it in GitHub Desktop.
Simplest implementation of Android in-app updates for Unity
using System;
using UnityEngine;
public sealed class AutoUpdater : MonoBehaviour
{
private const string AndroidClass = "com.lagoonsoft.AutoUpdater";
#region static Singleton
private static AutoUpdater _instance;
private static AutoUpdater GetInstance()
{
if( _instance == null )
{
_instance = new GameObject("AutoUpdater").AddComponent<AutoUpdater> ();
DontDestroyOnLoad (_instance.gameObject);
}
return _instance;
}
#endregion
#region LISTENERS
public static Action OnInstalReady;
public static Action OnInstalAvailable;
public void OnInstallReady( string useless )
{
Debug.Log("[AutoUpdater] OnInstallReady" );
InstallAvailable = true;
OnInstalReady?.Invoke();
}
public void OnUpdateStatus( string versioncode )
{
Debug.Log("[AutoUpdater] OnUpdateStatus " + versioncode );
if( versioncode != "-1" && versioncode != "-2" )
{
UpdateAvailable = true;
OnInstalAvailable?.Invoke();
}
}
public void OnUpdating( string useless )
{
Debug.Log("[AutoUpdater] OnUpdating" );
}
public void OnDownloadProgress( string progress )
{
Debug.Log("[AutoUpdater] OnDownloadProgress " + progress );
}
public static bool InstallAvailable { get; private set; }
public static bool UpdateAvailable { get; private set; }
#endregion
/// <summary>
/// Start check for updates
/// </summary>
public static void Init()
{
GetInstance();
InstallAvailable = false;
UpdateAvailable = false;
#if UNITY_ANDROID && !UNITY_EDITOR
using( var cls = new AndroidJavaClass( AndroidClass ) ) { cls.CallStatic( "Init" ); }
#endif
}
/// <summary>
/// Start immediate update
/// </summary>
public static bool StartUpdateImmediate()
{
InstallAvailable = false;
UpdateAvailable = false;
#if UNITY_ANDROID && !UNITY_EDITOR
using( var cls = new AndroidJavaClass(AndroidClass) ) { return cls.CallStatic<bool>("ApplyImmediateUpdate"); }
#else
return false;
#endif
}
/// <summary>
/// Start felxible update
/// </summary>
public static bool ProposeUpdateFlexible()
{
InstallAvailable = false;
UpdateAvailable = false;
#if UNITY_ANDROID && !UNITY_EDITOR
using( var cls = new AndroidJavaClass(AndroidClass) ) { return cls.CallStatic<bool>("ApplyFlexUpdate"); }
#else
return false;
#endif
}
/// <summary>
/// Start updating the app that has finish download
/// </summary>
public static void CompleteUpdate()
{
InstallAvailable = false;
UpdateAvailable = false;
#if UNITY_ANDROID && !UNITY_EDITOR
using( var cls = new AndroidJavaClass(AndroidClass) ) { cls.CallStatic("CompleteUpdate"); }
#endif
}
/// <summary>
/// Update download status on app resume
/// </summary>
public static void OnResume()
{
InstallAvailable = false;
UpdateAvailable = false;
#if UNITY_ANDROID && !UNITY_EDITOR
using( var cls = new AndroidJavaClass(AndroidClass) ) { cls.CallStatic("OnResume"); }
#endif
}
}
package com.lagoonsoft;
import android.content.IntentSender;
import android.util.Log;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.play.core.appupdate.AppUpdateInfo;
import com.google.android.play.core.appupdate.AppUpdateManager;
import com.google.android.play.core.appupdate.AppUpdateManagerFactory;
import com.google.android.play.core.install.InstallState;
import com.google.android.play.core.install.model.AppUpdateType;
import com.google.android.play.core.install.model.InstallStatus;
import com.google.android.play.core.install.model.UpdateAvailability;
import com.google.android.play.core.tasks.Task;
import com.unity3d.player.UnityPlayer;
import java.util.Random;
public class AutoUpdater
{
private static final String TAG = "AutoUpdater";
private static final String ON_INSTALL_READY = "OnInstallReady";
private static final String ON_UPDATE_STATUS = "OnUpdateStatus";
private static final String ON_UPDATING = "OnUpdating";
private static final String ON_DOWNLOADING = "OnDownloadProgress";
private static AutoUpdater _instance;
private AppUpdateManager appUpdateManager;
private AppUpdateInfo appUpdateInfo;
private final int MY_REQUEST_CODE;
private boolean hasFlexUpdateAvailable()
{
return appUpdateInfo != null && appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE);
}
private boolean hasImmediateUpdateAvailable()
{
return appUpdateInfo != null && appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE);
}
private int getAvailableVersionCode()
{
return appUpdateInfo != null && appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE ? appUpdateInfo.availableVersionCode() : -1;
}
/**
* singleton class
*/
private AutoUpdater()
{
Random rand = new Random();
MY_REQUEST_CODE = rand.nextInt();
}
/**
* Check if updates are available
*/
private void _init()
{
log("_init");
// Creates instance of the manager if not exists.
if( appUpdateManager == null )
appUpdateManager = AppUpdateManagerFactory.create( UnityPlayer.currentActivity );
// Returns an intent object that you use to check for an update.
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();
// Checks that the platform will allow the specified type of update.
appUpdateInfoTask.addOnSuccessListener( updateInfo ->
{
log(" -> OnSuccessListener");
appUpdateInfo = updateInfo;
// Update waiting
if( appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED )
{
log(" -> DOWNLOADED: " + getAvailableVersionCode() );
UnityPlayer.UnitySendMessage("AutoUpdater", ON_INSTALL_READY, "0" );
}
// Update available
else if( appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE )
{
log(" -> UPDATE_AVAILABLE: " + getAvailableVersionCode() );
UnityPlayer.UnitySendMessage("AutoUpdater", ON_UPDATE_STATUS, ""+appUpdateInfo.availableVersionCode() );
}
// Immediate update in progress
else if( appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS )
{
log(" -> DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS: " + getAvailableVersionCode() );
// If an in-app update is already running, resume the update.
_applyImmediateUpdate();
UnityPlayer.UnitySendMessage("AutoUpdater", ON_UPDATING, "0" );
}
// none
else
{
log(" -> NO UPDATES" );
UnityPlayer.UnitySendMessage("AutoUpdater", ON_UPDATE_STATUS, "-1" );
}
});
// check for errors
appUpdateInfoTask.addOnFailureListener( fail ->
{
UnityPlayer.UnitySendMessage("AutoUpdater", ON_UPDATE_STATUS, "-2" );
});
}
/**
* Singleton
* @return AutoUpdater
*/
private static AutoUpdater getInstance()
{
if( _instance == null )
{
_instance = new AutoUpdater();
}
return _instance;
}
private void _onStateUpdate( InstallState state )
{
log(" -> _onStateUpdate : " + state.installStatus() );
if( state.installStatus() == InstallStatus.DOWNLOADED )
{
appUpdateManager.unregisterListener( this::_onStateUpdate );
_init();
}
else if( state.installStatus() == InstallStatus.DOWNLOADING )
{
long bytesDownloaded = state.bytesDownloaded();
long totalBytesToDownload = state.totalBytesToDownload();
long progress = bytesDownloaded / totalBytesToDownload;
UnityPlayer.UnitySendMessage("AutoUpdater", ON_DOWNLOADING, String.format("Value: {0:P2}.", progress) );
}
}
private void _onResume()
{
_init();
}
private void _completeUpdate()
{
log(" -> _completeUpdate" );
if( appUpdateManager == null )
{
log(" -> impossible appUpdateManager == null" );
}
else if( appUpdateInfo == null )
{
log(" -> impossible appUpdateInfo == null" );
return;
}
else if( appUpdateInfo.installStatus() != InstallStatus.DOWNLOADED )
{
log(" -> impossible appUpdateInfo.installStatus() != InstallStatus.DOWNLOADED" );
return;
}
appUpdateManager.completeUpdate();
}
private boolean _applyFlexUpdate()
{
log(" -> _applyFlexUpdate " + hasFlexUpdateAvailable() );
if( hasFlexUpdateAvailable() )
{
try
{
appUpdateManager.registerListener( this::_onStateUpdate );
appUpdateManager.startUpdateFlowForResult(
// Pass the intent that is returned by 'getAppUpdateInfo()'.
appUpdateInfo,
// Or 'AppUpdateType.IMMEDIATE' for immediate updates.
AppUpdateType.FLEXIBLE,
// The current activity making the update request.
UnityPlayer.currentActivity,
// Include a request code to later monitor this update request.
MY_REQUEST_CODE);
return true;
}
catch( IntentSender.SendIntentException e )
{
e.printStackTrace();
}
}
return false;
}
private boolean _applyImmediateUpdate()
{
log(" -> _applyImmediateUpdate " + hasImmediateUpdateAvailable() );
if( hasImmediateUpdateAvailable() )
{
try
{
appUpdateManager.registerListener( this::_onStateUpdate );
appUpdateManager.startUpdateFlowForResult(
// Pass the intent that is returned by 'getAppUpdateInfo()'.
appUpdateInfo,
// Or 'AppUpdateType.FLEXIBLE' for flexible updates.
AppUpdateType.IMMEDIATE,
// The current activity making the update request.
UnityPlayer.currentActivity,
// Include a request code to later monitor this update request.
MY_REQUEST_CODE);
return true;
}
catch( IntentSender.SendIntentException e )
{
e.printStackTrace();
}
}
return false;
}
// ----------------------------------------------------------------------------------------------------------------|
// -- STATICS UNITY FUNCTIONS -- ----------------------------------------------------------------------------------|
// ----------------------------------------------------------------------------------------------------------------|
/**
* Check if updates are available
*/
public static void Init()
{
getInstance()._init();
}
/**
* Launch FlexibleUpdate
* @return boolean
*/
public static boolean ApplyFlexUpdate()
{
return getInstance()._applyFlexUpdate();
}
/**
* Launch ImmediateUpdate
* @return boolean
*/
public static boolean ApplyImmediateUpdate()
{
return getInstance()._applyImmediateUpdate();
}
/**
* Start installing downloaded flexible update
*/
public static void CompleteUpdate()
{
getInstance()._completeUpdate();
}
/**
* returns true if a FlexibleUpdate is available
*/
public static boolean FlexUpdateDisponible()
{
return getInstance().hasFlexUpdateAvailable();
}
/**
* returns true if an ImmediateUpdate is available
*/
public static boolean ImmediateUpdateDisponible()
{
return getInstance().hasImmediateUpdateAvailable();
}
/**
* returns the version number of the available update
*/
public static int GetVersionDisponible()
{
return getInstance().getAvailableVersionCode();
}
/**
* relaunch checks on app resumes
*/
public static void OnResume()
{
getInstance()._onResume();
}
/**
* logs
* @param msg String
*/
private void log( String msg )
{
Log.d(TAG, msg);
}
}
<!-- ADD this in an Editor folder -->
<dependencies>
<androidPackages>
<androidPackage spec="com.google.android.material:material:1.0.+"/>
<androidPackage spec="com.google.android.play:core:1.6.+"/>
</androidPackages>
</dependencies>
@mehdi-shamsi
Copy link

could you please say more detail, like where should exactly AutoUpdater.java file and can we create an editor folder under asset/scripts folder and put AutoUpdaterDependencies.xml inside of it?

@binouze
Copy link
Author

binouze commented May 8, 2023

Hi, you can create a folder AutoUpdater inside Assets/Plugins folder and put all files inside.
This Gist is a little old, Google published their own implementation of InAppUpdates for Unity, I think it's better to use this one ;)
https://developer.android.com/guide/playcore/in-app-updates/unity

@mehdi-shamsi
Copy link

yes, but I couldn't find complete E2E codes. and goggling result returns some incomplete solutions.
can you help me?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment