카테고리 보관물: Android

Unreal 5.4 안드로이드 aab 파일 생성 시 결제 라이브러리 제거

개요

Unreal 5.4 로 게임 개발을 마치고 Android aab 파일 생성 후 업로드 하면 결제 라이브러리 관련한 오류가 발생했습니다. 게임 내에 결제 요소가 없는데 기본적으로 결제 라이브러리가 사용되도록 되어 있어 발생한 것 같았습니다. 이 문제를 해결하는 방법을 알아보도록 하겠습니다.

오류 메시지

앱을 업로드 하면 다음과 같은 오류 메시지가 나타납니다.

앱에서 현재 Play 결제 라이브러리 AIDL 버전을 사용 중입니다. Google Play의 최신 수익 창출 기능을 사용하려면 5.2.1 버전 이상으로 업데이트해야 합니다.

실제 결제 기능이 있다면 버전을 업데이트 하는 작업을 진행해야 합니다. 필자가 제작한 게임에는 결제 기능이 없어서 처음에는 이상하다고 생각했습니다.

문제 파악

이리 저리 알아보니 인앱결제 사용 여부를 지정하도록 되어 있는 것 같았습니다. 그 값이 기본으로 사용하도록 되어 있는 것 같았습니다. 설정을 모두 찾아보았으나 인앱결제를 사용하는 여부를 선택하는 항목은 없었습니다.

C:\YourInstallPath\UE_5.4\Engine\Plugins\Online\Android\OnlineSubsystemGooglePlay\Source\OnlineSubsystemGooglePlay_UPL.xml 파일을 살펴보면 다음과 같은 내용이 있습니다.

<if condition="bSupportsInAppPurchasing">
...
</if>

이런 부분이 여러 곳에 있는데 조건에 따라 결제에 필요한 사항을 추가하는 것으로 생각되었습니다. bSupportsInAppPurchasing 이 항목의 값을 False 로 변경하면 문제가 해결될 것 같았습니다.

해결방법

YourProject\Config\Android\AndroidEngine.ini 파일을 수정합니다. 필자의 경우 없어서 생성했습니다. 파일의 내용을 다음과 같이 입력합니다.

[OnlineSubsystem]
DefaultPlatformService=GooglePlay

[OnlineSubsystemGooglePlay.Store]
bSupportsInAppPurchasing=False

bSupportsInAppPurchasing 항목을 False 로 지정하면 됩니다. 결제 기능을 사용한다면 True 로 설정해야 합니다. OnlineSubsystemGooglePlay_UPL.xml 파일 내의 bSupportsInAppPurchasing 항목과 일치하는 것을 알 수 있습니다.

다시 aab파일을 다시 업로드하면 오류가 사라집니다.

Unreal Android aab 파일 업로드 시 경고 해결

개요

Unreal 로 개발을 마치고 플레이 스토어에 aab 파일을 올렸는데 경고 메시지가 나타났습니다. 무시해도 당분간은 문제가 없습니다. 하지만 업데이트는 계속해야 하고 결국 해결해야 할 문제라는 생각이 들어 조치하기로 했습니다.

첫 번째 경고 메시지

첫 번째 경고 메시지는 다음과 같습니다.

androidx.fragment:fragment (androidx.fragment:fragment) 개발자가 1.0.0 버전이 오래되었다고 신고했습니다. 신작을 게시하기 전에 다음 버전 중 하나로 업그레이드하는 것이 좋습니다.1.1.0+ 사용 중인 SDK에 대해 자세히 알아보고 Google Play SDK 색인 정보를 바탕으로 SDK를 선택하세요.

내용 그대로 오래된 버전으로 지정되어 발생한 경고입니다. Engine\Source\ThirdParty\AndroidPermission\permission_library\additions.gradle 파일을 열어서 다음의 내용을 추가합니다.

dependencies {
    ...
    constraints.implementation 'androidx.fragment:fragment:1.3.6'
}

Unreal Engine 5.3.2 기준으로 fragment 버전을 최신(1.6.2)으로 하면 빌드 오류가 발생했습니다. 1.3.6 으로 지정하면 정상적으로 빌드됩니다.

나머지 경고 메시지

앱 화면 하단에 배너 광고가 나타나도록 했습니다. 다른 경고 메시지는 애드몹(Admob)에 관련된 것 이었습니다.

Google Mobile Ads (GMA) SDK (com.google.android.gms:play-services-ads) 개발자가 18.1.0 버전이 오래되었다고 신고했습니다. 이 버전의 앱을 출시한 지 90일이 지나면 새 버전(20.0.0+)으로 업그레이드할 때까지 이 SDK를 포함한 새 버전을 출시할 수 없습니다. 사용 중인 SDK에 대해 자세히 알아보고 Google Play SDK 색인 정보를 바탕으로 SDK를 선택하세요.

Google Mobile Ads (GMA) SDK (com.google.android.gms:play-services-ads) 개발자가 SDK 버전 18.1.0에 다음 메모를 추가했습니다. As of June 30th 2023, this version is sunset. For more information, please visit https://developers.google.com/admob/android/deprecation. 이 버전의 앱을 출시한 지 90일이 지나면 새 버전으로 업그레이드할 때까지 이 SDK를 포함한 새 버전을 출시할 수 없습니다. 사용 중인 SDK에 대해 자세히 알아보고 Google Play SDK 색인 정보를 바탕으로 SDK를 선택하세요.

Google Mobile Ads (GMA) SDK (com.google.android.gms:play-services-ads-lite) 개발자가 18.1.0 버전이 오래되었다고 신고했습니다. 이 버전의 앱을 출시한 지 90일이 지나면 새 버전(20.0.0+)으로 업그레이드할 때까지 이 SDK를 포함한 새 버전을 출시할 수 없습니다. 사용 중인 SDK에 대해 자세히 알아보고 Google Play SDK 색인 정보를 바탕으로 SDK를 선택하세요.

나머지 경고 메시지 해결 방법

이 글에서 안내하는 해결 방법은 배너 광고에만 해당합니다. Engine\Source\Runtime\Advertising\Android\AndroidAdvertising\AndroidAdvertising_APL.xml 파일을 열어 implementation(‘com.google.android.gms:play-services-ads:18.0.1’) 을 implementation(‘com.google.android.gms:play-services-ads:22.6.0’) 로 변경합니다.

이 상태에서 빌드하면 InterstitialAd 관련 오류가 발생합니다. 원인은 com.google.android.gms.ads.InterstitialAd 이 20.0.0+ 에서 deprecated 되어 AndroidAdvertising_APL.xml 파일내의 삽입광고 구현부분과 라이브러리 버전 20.0.0+ 과의 불일치 때문입니다.

삽입광고를 사용하시는 분들은 새로 변경된 라이브러리 버전에 맞추어 수정하셔야 합니다. <![CDATA[ , ]]>로 감싸진 부분이 애드몹 관련 자바 코드 부분입니다. 필자는 삽입 광고 부분을 모두 주석 처리 했습니다. 추가로 더 이상 지원되지 않는 메소드의 @Override 부분도 다음과 같이 주석 처리 했습니다.

/*
@Override
public void onAdFailedToLoad(int errorCode)
{
    adIsAvailable = false;
    adIsRequested = false;

    // don't immediately request a new ad on failure, wait until the next show
    updateAdVisibility(false);
}
*/

AndroidAdvertising_APL.xml 파일을 수정한 경우 Unreal Engine이 실행 중이면 바로 반영되지 않습니다. 다시 엔진을 시작하고 빌드한 후 aab 파일을 올리면 위의 경고 메시지가 나타나지 않는 것을 알 수 있습니다.

삽입 광고 관련한 부분을 변경된 라이브러리에 맞추어 수정된 파일이 있지 않을까 해서 검색을 좀 해보았는데 발견하지 못하였습니다. 엔진이 업데이트 되면 이 불일치 부분도 해결되었으면 좋겠습니다.

참고로 AndroidAdvertising_APL.xml 파일의 수정된 부분 전체의 내용입니다.

/* 위쪽 import 부분 */
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.ads.MobileAds;
import com.google.android.gms.ads.initialization.InitializationStatus;
import com.google.android.gms.ads.initialization.OnInitializationCompleteListener;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.AdView;
import com.google.android.gms.ads.AdSize;
import com.google.android.gms.ads.AdListener;
//import com.google.android.gms.ads.InterstitialAd;
import com.google.android.gms.ads.interstitial.InterstitialAd;
import com.google.android.gms.ads.RequestConfiguration;
import com.google.android.gms.ads.identifier.AdvertisingIdClient;
import com.google.android.gms.ads.identifier.AdvertisingIdClient.Info;

    /** AdMob support */
    private PopupWindow adPopupWindow;
    private AdView adView;
    private boolean adInit = false;
    private LinearLayout adLayout;
    private int adGravity = Gravity.TOP;
    //private InterstitialAd interstitialAd;
    //private boolean isInterstitialAdLoaded = false;
    //private boolean isInterstitialAdRequested = false;
    //private AdRequest interstitialAdRequest;
    private String advertisingID = null;

	/** true when the application has requested that an ad be displayed */
	private boolean adWantsToBeShown = false;

	/** true when an ad is available to be displayed */
	private boolean adIsAvailable = false;

	/** true when an ad request is in flight */
	private boolean adIsRequested = false;

	// handle ad popup visibility and requests
	private void updateAdVisibility(boolean loadIfNeeded)
	{
		if (!adInit || (adPopupWindow == null))
		{
			return;
		}

		// request an ad if we don't have one available or requested, but would like one
		if (adWantsToBeShown && !adIsAvailable && !adIsRequested && loadIfNeeded)
		{
			AdRequest adRequest = new AdRequest.Builder().build();		// add test devices here
			_activity.adView.loadAd(adRequest);

			adIsRequested = true;
		}

		if (adIsAvailable && adWantsToBeShown)
		{
			if (adPopupWindow.isShowing())
			{
				return;
			}

			adPopupWindow.showAtLocation(activityLayout, adGravity, 0, 0);
			// don't call update on 7.0 to work around this issue: https://code.google.com/p/android/issues/detail?id=221001
			if (ANDROID_BUILD_VERSION != 24) {
				adPopupWindow.update();
			}
		}
		else
		{
			if (!adPopupWindow.isShowing())
			{
				return;
			}

			adPopupWindow.dismiss();
			adPopupWindow.update();
		}
	}

	public void AndroidThunkJava_ShowAdBanner(String AdMobAdUnitID, boolean bShowOnBottonOfScreen)
	{
		Log.debug("In AndroidThunkJava_ShowAdBanner");
		Log.debug("AdID: " + AdMobAdUnitID);

		adGravity = bShowOnBottonOfScreen ? Gravity.BOTTOM : Gravity.TOP;

		if (adInit)
		{
			// already created, make it visible
			_activity.runOnUiThread(new Runnable()
			{
				@Override
				public void run()
				{
					if ((adPopupWindow == null) || adPopupWindow.isShowing())
					{
						return;
					}

					adWantsToBeShown = true;
					updateAdVisibility(true);
				}
			});

			return;
		}

		// init our AdMob window
		adView = new AdView(this);
		adView.setAdUnitId(AdMobAdUnitID);
		adView.setAdSize(AdSize.BANNER);

		if (adView != null)
		{
			_activity.runOnUiThread(new Runnable()
			{
				@Override
				public void run()
				{
					adInit = true;

					final DisplayMetrics dm = getResources().getDisplayMetrics();
					final float scale = dm.density;
					adPopupWindow = new PopupWindow(_activity);
					adPopupWindow.setWidth((int)(320*scale));
					adPopupWindow.setHeight((int)(50*scale));
					adPopupWindow.setClippingEnabled(false);

					adLayout = new LinearLayout(_activity);

					final int padding = (int)(-5*scale);
					adLayout.setPadding(padding,padding,padding,padding);

					MarginLayoutParams params = new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);;

					params.setMargins(0,0,0,0);

					adLayout.setOrientation(LinearLayout.VERTICAL);
					adLayout.addView(adView, params);
					adPopupWindow.setContentView(adLayout);

					// set up our ad callbacks
					_activity.adView.setAdListener(new AdListener()
					{
						 @Override
						public void onAdLoaded()
						{
							adIsAvailable = true;
							adIsRequested = false;

							updateAdVisibility(true);
						}

						/*
						 @Override
						public void onAdFailedToLoad(int errorCode)
						{
							adIsAvailable = false;
							adIsRequested = false;

							// don't immediately request a new ad on failure, wait until the next show
							updateAdVisibility(false);
						}
						*/
					});

					adWantsToBeShown = true;
					updateAdVisibility(true);
				}
			});
		}
	}

	public void AndroidThunkJava_HideAdBanner()
	{
		Log.debug("In AndroidThunkJava_HideAdBanner");

		if (!adInit)
		{
			return;
		}

		_activity.runOnUiThread(new Runnable()
		{
			@Override
			public void run()
			{
				adWantsToBeShown = false;
				updateAdVisibility(true);
			}
		});
	}

	public void AndroidThunkJava_CloseAdBanner()
	{
		Log.debug("In AndroidThunkJava_CloseAdBanner");

		if (!adInit)
		{
			return;
		}

		// currently the same as hide.  should we do a full teardown?
		_activity.runOnUiThread(new Runnable()
		{
			@Override
			public void run()
			{
				adWantsToBeShown = false;
				updateAdVisibility(true);
			}
		});
	}

	public void AndroidThunkJava_LoadInterstitialAd(String AdMobAdUnitID)
	{
		/*
		interstitialAdRequest = new AdRequest.Builder().build();

		interstitialAd = new InterstitialAd(this);
		isInterstitialAdLoaded = false;
		isInterstitialAdRequested = true;
		interstitialAd.setAdUnitId(AdMobAdUnitID);

		_activity.runOnUiThread(new Runnable()
		{
			@Override
			public void run()
			{
				interstitialAd.loadAd(interstitialAdRequest);
			}
		});

		interstitialAd.setAdListener(new AdListener()
		{
			@Override
			public void onAdFailedToLoad(int errorCode)
			{
				Log.debug("Interstitial Ad failed to load, errocode: " + errorCode);
				isInterstitialAdLoaded = false;
				isInterstitialAdRequested = false;
			}
			@Override
			public void onAdLoaded()
			{
				//track if the ad is loaded since we can only called interstitialAd.isLoaded() from the uiThread
				isInterstitialAdLoaded = true;
				isInterstitialAdRequested = false;
			}
		});
		*/
	}

	public boolean AndroidThunkJava_IsInterstitialAdAvailable()
	{
		//return interstitialAd != null && isInterstitialAdLoaded;
		return false;
	}

	public boolean AndroidThunkJava_IsInterstitialAdRequested()
	{
		//return interstitialAd != null && isInterstitialAdRequested;
		return false;
	}

	public void AndroidThunkJava_ShowInterstitialAd()
	{
		/*
		if(isInterstitialAdLoaded)
		{
			_activity.runOnUiThread(new Runnable()
			{
				@Override
				public void run()
				{
					interstitialAd.show();
				}
			});
		}
		else
		{
			Log.debug("Interstitial Ad is not available to show - call LoadInterstitialAd or wait for it to finish loading");
		}
		*/
	}

	private GetAdvertisingIdTask AdTask = null;

	private class GetAdvertisingIdTask extends android.os.AsyncTask<String, Integer, String>
	{
		@Override
		protected String doInBackground(String... values)
		{
			AdvertisingIdClient.Info adInfo = null;
			try
			{
				adInfo = AdvertisingIdClient.getAdvertisingIdInfo(GameActivity.Get().getApplicationContext());
				if (adInfo.isLimitAdTrackingEnabled())
				{
					Log.debug("GetAdvertisingId: User opted out of ad tracking");
					adInfo = null;
				}
				Log.debug("GetAdvertisingID: success");
			}
			catch (Exception e) {
				Log.debug("GetAdvertisingId failed: " + e.getMessage());
			}
			return (adInfo == null) ? "" : adInfo.getId();
		}

		@Override
		protected void onPostExecute(String s)
		{
			advertisingID = s;
		}
	}

	public String AndroidThunkJava_GetAdvertisingId()
	{
		try
		{
			AdTask.get();
		}
		catch (Exception e)
		{
			advertisingID = null;
		}

		return advertisingID;
	}
]]>

별 내용이 없어 보이는데 생각보다 해결에 시간이 많이 소요되었습니다. 필자와 같은 문제를 겪고 계신 분들께 도움이 되었으면 좋겠습니다.