// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.IO;
using Google.Android.AppBundle.Editor.Internal.AssetPacks;
using UnityEditor;
using UnityEngine;
#if UNITY_2018_1_OR_NEWER
using UnityEditor.Build.Reporting;
#endif
namespace Google.Android.AppBundle.Editor.Internal.BuildTools
{
///
/// Indicates the results of an build. This provides similar information as
/// Task<AndroidBuildReport> without requiring the .NET 4.0 async task API.
///
public class AndroidBuildResult
{
///
/// Returns true if Android Player build Succeeded, otherwise false.
///
public bool Succeeded
{
get { return !Cancelled && ErrorMessage == null; }
}
///
/// Returns true if the Android Player build was cancelled before it finished.
///
public bool Cancelled { get; internal set; }
///
/// If non-null, the build failed and this message may indicate the cause.
///
public string ErrorMessage { get; internal set; }
#if UNITY_2018_1_OR_NEWER
///
/// Contains the BuildReport generated during the creation of the Android Player.
///
public BuildReport Report { get; internal set; }
#endif
}
///
/// Provides methods for building an Android app for distribution on Google Play.
///
public class AndroidBuilder : IBuildTool
{
// This string is returned by pre-2018.1's BuildPipeline.BuildPlayer() when a build is cancelled.
private const string BuildCancelledMessage = "Building Player was cancelled";
private readonly AndroidSdkPlatform _androidSdkPlatform;
private BuildToolLogger _buildToolLogger;
public AndroidBuilder(AndroidSdkPlatform androidSdkPlatform)
{
_androidSdkPlatform = androidSdkPlatform;
}
// TODO(b/189958664): Needed for 1.x API compatibility. Should be removed with 2.x.
public AndroidBuilder(AndroidSdkPlatform androidSdkPlatform, ApkSigner apkSigner) : this(androidSdkPlatform)
{
}
public virtual bool Initialize(BuildToolLogger buildToolLogger)
{
_buildToolLogger = buildToolLogger;
if (EditorUserBuildSettings.androidBuildSystem != AndroidBuildSystem.Gradle)
{
// We require Gradle builds for APK Signature Scheme V2, which is required for Play Instant on Unity 2017+.
const string message = "This build requires the Gradle Build System.\n\n" +
"Click \"OK\" to change the Android Build System to Gradle.";
if (buildToolLogger.DisplayActionableErrorDialog(message))
{
EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
}
return false;
}
// TODO(b/154937088): remove this check if we add support for exporting a Gradle project.
if (EditorUserBuildSettings.exportAsGoogleAndroidProject)
{
const string message = "This build doesn't support exporting to Android Studio.\n\n" +
"Click \"OK\" to disable exporting a Gradle project.";
if (buildToolLogger.DisplayActionableErrorDialog(message))
{
EditorUserBuildSettings.exportAsGoogleAndroidProject = false;
}
return false;
}
if (BuiltInPadHelper.EditorSupportsPad() && BuiltInPadHelper.ProjectHasAndroidPacks())
{
var message =
"This build method doesn't support .androidpack folders. Assets within those folders will not be included" +
" in the build.";
buildToolLogger.DisplayOptOutDialog(message, "androidPackError");
}
if (PlayerSettings.Android.splitApplicationBinary)
{
string messagePrefix;
if (BuiltInPadHelper.EditorSupportsPad())
{
messagePrefix = "This build method doesn't support Unity's \"Split Application Binary\" option.";
}
else
{
messagePrefix = "This build method doesn't support APK Expansion (OBB) files.";
}
var message = string.Format(
"{0}\n\nLarge games can instead use the " +
"\"{1}\" option available through the \"Google > Android App Bundle > Asset Delivery " +
"Settings\" menu or the AssetPackConfig API's SplitBaseModuleAssets field.\n\n" +
"Click \"OK\" to disable the \"Split Application Binary\" setting.",
messagePrefix,
AssetDeliveryWindow.SeparateAssetsLabel);
if (buildToolLogger.DisplayActionableErrorDialog(message))
{
PlayerSettings.Android.splitApplicationBinary = false;
}
return false;
}
return _androidSdkPlatform.Initialize(buildToolLogger);
}
// TODO(b/189958664): Needed for 1.x API compatibility. Should be removed with 2.x.
public virtual bool BuildAndSign(BuildPlayerOptions buildPlayerOptions)
{
return Build(buildPlayerOptions).Succeeded;
}
///
/// Builds an APK or AAB based on the specified options.
/// Displays warning/error dialogs if there are issues during the build.
///
public virtual AndroidBuildResult Build(BuildPlayerOptions buildPlayerOptions)
{
if (_buildToolLogger == null)
{
throw new BuildToolNotInitializedException(this);
}
if (buildPlayerOptions.target != BuildTarget.Android)
{
throw new ArgumentException("The build target must be Android.", "buildPlayerOptions");
}
if (buildPlayerOptions.targetGroup != BuildTargetGroup.Android)
{
throw new ArgumentException("The build target group must be Android.", "buildPlayerOptions");
}
// Note: the type of the variable below differs by version. On 2018+ it's BuildReport. On pre-2018 it's
// string: if the string is null, the build was successful, otherwise it's a build error message.
var buildReportOrErrorMessage = BuildPipeline.BuildPlayer(buildPlayerOptions);
var androidBuildResult = GetAndroidBuildResult(buildReportOrErrorMessage);
if (androidBuildResult.Cancelled)
{
// Don't display an error message dialog if the user asked to cancel, just log to the Console.
Debug.Log(BuildCancelledMessage);
return androidBuildResult;
}
if (androidBuildResult.Succeeded && !File.Exists(buildPlayerOptions.locationPathName))
{
// Sometimes the build "succeeds" but the AAB/APK file is missing.
androidBuildResult.ErrorMessage =
string.Format(
"The Android Player file \"{0}\" is missing, possibly because of a late cancellation.",
buildPlayerOptions.locationPathName);
#if UNITY_2018_1_OR_NEWER
androidBuildResult.ErrorMessage += " TotalErrors=" + buildReportOrErrorMessage.summary.totalErrors;
#endif
}
if (androidBuildResult.Succeeded)
{
Debug.Log("Android Player build succeeded");
}
else
{
_buildToolLogger.DisplayErrorDialog(androidBuildResult.ErrorMessage);
}
return androidBuildResult;
}
#if UNITY_2018_1_OR_NEWER
private static AndroidBuildResult GetAndroidBuildResult(BuildReport buildReport)
{
var androidBuildResult = new AndroidBuildResult();
androidBuildResult.Report = buildReport;
switch (buildReport.summary.result)
{
case BuildResult.Succeeded:
// Do nothing.
break;
case BuildResult.Cancelled:
androidBuildResult.Cancelled = true;
break;
case BuildResult.Failed:
androidBuildResult.ErrorMessage =
string.Format("Build failed with {0} error(s)", buildReport.summary.totalErrors);
break;
case BuildResult.Unknown:
androidBuildResult.ErrorMessage = "Build failed with unknown result";
break;
default:
androidBuildResult.ErrorMessage =
"Build failed with unexpected result: " + buildReport.summary.result;
break;
}
return androidBuildResult;
}
#else
private static AndroidBuildResult GetAndroidBuildResult(string errorMessage)
{
var androidBuildResult = new AndroidBuildResult();
if (errorMessage == BuildCancelledMessage)
{
androidBuildResult.Cancelled = true;
}
else if (!string.IsNullOrEmpty(errorMessage))
{
// Assume that a null or empty error message string indicates success.
androidBuildResult.ErrorMessage = errorMessage;
}
return androidBuildResult;
}
#endif
}
}