태그 보관물: android

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;
	}
]]>

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

애드몹(Admob) 의도하지 않은 클릭을 유도하는 레이아웃 – 예기치 않은 전면 광고 실행 조치 방법

개요

간단한 기능을 하는 앱에 애드몹 전면광고가 삽입되어 있었습니다. 어느 날 “귀하의 앱에서 Google AdMob 광고 게재가 중지되었습니다” 라는 제목으로 메일이 왔습니다. 내용을 살펴보니 “의도하지 않은 클릭을 유도하는 레이아웃 – 예기치 않은 전면 광고 실행” 이라고 안내되어 있었습니다. 영문으로는 Layout Encourages Accidental Clicks – Unexpected Launch Interstitials 입니다.

내용 파악

복잡한 기능을 하는 앱이 아니어서 무엇이 문제인지 처음에는 잘 몰랐습니다. 생각해 보니 전면광고가 나타나는 시점을 약간 조정했는데 그것이 문제가 된 것 같았습니다. 우측 상단의 설정 버튼을 제외하고 화면상에 터치가 필요한 앱이 아니었습니다.

처음 앱이 실행되었을 때 사용자가 무심코 화면을 터치하려는 순간 전면광고가 나타나서 의도하지 않은 광고 클릭이 될 수 있다는 생각이 들었습니다. 당연히 의도한 것은 아니었고 가능성은 낮지만 충분히 일어날 수 일이라는 판단이 들어 조치에 들어갔습니다.

첫 번째 조치

지연 시간을 없애고 광고가 완전히 준비된 후 표시가 가능할 때 나타나도록 수정했습니다. 지연 시간을 둔 이유는 광고가 나타나지 않는 현상이 있어서 그랬는데 결과적으로 문제가 되었습니다.

앱 수정 및 업데이트하고 반영된 것을 확인 후 검토 요청을 했습니다. 그러나 거부 메일이 또 다른 위반 사항이 추가되어 도착했습니다.

“게시자 콘텐츠보다 광고나 기타 유료 프로모션 자료가 더 많은 화면 이라는 위반 사항이 추가되었습니다.” 라는 내용이었습니다. 자세한 사항은 이곳에서 확인하실 수 있습니다. 요약하자면 앱에서 제공하는 내용보다 광고가 차지하는 영역이 많다는 것 입니다. 영문으로는 More ads or paid promotional material than publisher-content 입니다.

두 번째 조치

두 개의 문제를 어떻게 해결해야 할지 난감했습니다. 무엇이 문제일까 고민을 해 보았는데 광고가 나타나는 시점을 사용자가 예측할 수 없다는 것이 제일 큰 것 같았습니다. 가장 먼저 앱의 기능이 화면에 표시되고 그 후에 광고가 나타나는데 상황에 따라 빠를 수도 늦을 수도 있을 것 같았습니다.

고민 끝에 앱 기능과 광고가 나타나는 시점을 아예 분리하기로 했습니다. 광고의 준비, 표시, 실패 등 관련된 이벤트가 발생한 후 앱의 본 기능이 작동하도록 수정했습니다. 그리고 추가된 위반사항에 대한 것도 앱의 내용이 나타나기 전에 광고가 나타나므로 문제가 해결될 것 같았습니다. 다시 업데이트하고 반영된 것을 확인한 후 다시 검토 요청했습니다.

대략 한 시간 조금 지나서 문제가 없다는 메일을 받았고 앱에서 전면광고가 나타나는 것을 확인했습니다.

위반 해결 알림 메일

구글에서 위반 메일을 받으면 당황하게 됩니다(필자는 그랬습니다). 약간 막연하지만 차분히 생각해 보면 대부분 해결책을 찾을 수 있을 것 입니다.

위에 언급한 두 가지 위반 사항으로 고민하시는 분들께 도움이 되었으면 합니다.