blob: 47285bb38055315b5099684390c34cbe24c477dd [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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
*
* http://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.
*/
package com.google.appindexing.fetchasgoogle;
import static com.android.tools.idea.projectsystem.gradle.GradleProjectSystemKt.getBuiltApksForSelectedVariant;
import com.android.annotations.VisibleForTesting;
import com.android.tools.idea.gradle.project.build.invoker.AssembleInvocationResult;
import com.android.tools.idea.gradle.project.build.invoker.GradleBuildInvoker;
import com.android.tools.idea.gradle.project.build.invoker.TestCompileType;
import com.android.tools.idea.model.AndroidModel;
import com.android.tools.idea.run.ApkInfo;
import com.google.api.services.fetchasgoogle_pa.model.ApkHolder;
import com.google.api.services.fetchasgoogle_pa.model.FetchResponse;
import com.google.appindexing.editor.AppIndexingVirtualFile;
import com.google.appindexing.fetchasgoogle.FetchAsGoogleClient.FetchAsGoogleException;
import com.google.appindexing.util.AppIndexingBundle;
import com.google.common.util.concurrent.ListenableFuture;
import com.intellij.openapi.application.ActionsKt;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A runnable task which execute a Fetch as Google request, including building apk, uploading and fetching result.
*/
public class FetchAsGoogleTask implements Runnable {
private static final Logger logger = Logger.getInstance(FetchAsGoogleTask.class);
private final boolean myBuildApkToRun;
private final RequestType myRequestType;
private Status myStatus = Status.NOT_STARTED;
private ErrorCode myErrorCode;
private Project myProject;
private String myDeepLink;
private AppIndexingVirtualFile myVirtualFile;
private FetchAsGoogleClient myFetchAsGoogleClient;
private ApkHolder myApkHolder;
private FetchResponse myFetchResponse;
private Date myCreatedTime;
private UploadedApkManager myUploadedApkManager;
private Helper myHelper;
private String myPackageId;
/**
* @return package id of the app for test. It could be null if the package id is failed to get from app manifest file.
*/
@Nullable
public String getPackageId() {
return myPackageId;
}
@NotNull
public RequestType getRequestType() {
return myRequestType;
}
public enum Status {
NOT_STARTED,
BUILDING_APK, // Building the apk
UPLOADING_APK, // Uploading the apk to backend server.
FETCHING_RESULT, // Waiting the result.
SUCCESS,
FAIL
}
// Can't rename:
// external error code instantiated by name via ErrorCode.valueOF(myFetchResponse.getStatus())
@SuppressWarnings("WrongTerminology")
public enum ErrorCode {
UNKNOWN,
BUILD_APK_FAILED,
UPLOAD_APK_FAILED,
FETCH_RESULT_FAILED,
NEED_SIGNIN,
NETWORK_ERROR,
URI_NOT_SUPPORTED,
UNKNOWN_URL,
UNKNOWN_APP_URI,
APK_NOT_FOUND,
BLACKLISTED_URL,
URL_INDEX_ERROR,
EMPTY_PACKAGE_ID,
// Error codes for checking app indexing errors
USER_ACTION_LOGGING_ERROR,
PERSONAL_CONTENT_INDEXING_ERROR,
BACKEND_ERROR,
RESOURCE_EXHAUSTED,
NOT_FOUND,
APP_OWNERSHIP_VERIFICATION_FAILED,
OK;
private final String message;
ErrorCode() {
this.message = AppIndexingBundle.message("app.indexing.fetch.as.google.test.error." + name());
}
public String getName() {
return super.name();
}
public String getMessage() {
return message;
}
}
public enum RequestType {
UNKNOWN_PREVIEW_FEATURE,
APP_INDEXING,
INSTANT_APP,
USER_ACTION_LOGGING_DEBUGGING,
PERSONAL_CONTENT_INDEXING_DEBUGGING
}
static public FetchAsGoogleTask createFetchAsGoogleTask(
@NotNull Project project, @NotNull Module module, @NotNull String deepLink, boolean buildApk, @NotNull RequestType type)
throws FetchAsGoogleException {
return new FetchAsGoogleTask(
project, deepLink, FetchAsGoogleClient.createInstance(), UploadedApkManager.INSTANCE, new Helper(project, module), buildApk, type);
}
@VisibleForTesting
FetchAsGoogleTask(@NotNull Project project, @NotNull String deepLink,
@NotNull FetchAsGoogleClient fetchAsGoogleClient, @NotNull UploadedApkManager uploadedApkManager,
@NotNull Helper helper, boolean buildApk, @NotNull RequestType type) {
myProject = project;
myDeepLink = deepLink;
myCreatedTime = new Date();
myFetchAsGoogleClient = fetchAsGoogleClient;
myUploadedApkManager = uploadedApkManager;
myHelper = helper;
myBuildApkToRun = buildApk;
myRequestType = type;
}
@NotNull
public Status getStatus() {
return myStatus;
}
@NotNull
public Module getModule() {
return myHelper.myModule;
}
@NotNull
public Project getProject() {
return myProject;
}
/**
* @return error code of the task. If there is no error, just return null.
*/
@Nullable
public ErrorCode getErrorCode() {
return myErrorCode;
}
@NotNull
public Date getCreatedTime() {
return myCreatedTime;
}
@Nullable
public FetchResponse getFetchResponse() {
return myFetchResponse;
}
private void changeStatus(@NotNull Status newStatus) {
changeStatus(newStatus, null);
}
/**
* Notify Result Panel of the latest status of the task to update UI.
*/
private void changeStatus(@NotNull Status newStatus, @Nullable ErrorCode errorCode) {
myStatus = newStatus;
if (errorCode != null) {
myErrorCode = errorCode;
}
Logger.getInstance(FetchAsGoogleTask.class).warn(newStatus.toString());
ApplicationManager.getApplication().invokeAndWait(() -> myVirtualFile.fireContentChange(), ModalityState.any());
}
@Override
public void run() {
myVirtualFile = new AppIndexingVirtualFile(myDeepLink, this);
ApplicationManager.getApplication().invokeAndWait(() -> {
FileEditorManager fileEditorManager = FileEditorManager.getInstance(myProject);
fileEditorManager.openFile(myVirtualFile, true);
}, ModalityState.any());
try {
if (RequestType.PERSONAL_CONTENT_INDEXING_DEBUGGING == myRequestType ||
RequestType.USER_ACTION_LOGGING_DEBUGGING == myRequestType) {
changeStatus(Status.FETCHING_RESULT);
myPackageId = myHelper.getPackageId();
if (myPackageId != null) {
myFetchResponse = myFetchAsGoogleClient.getAppIndexingErrorStats(myPackageId, myRequestType);
changeStatus(Status.SUCCESS, ErrorCode.valueOf(myFetchResponse.getStatus()));
}
else {
changeStatus(Status.FAIL, ErrorCode.EMPTY_PACKAGE_ID);
}
return;
}
// For app indexing preview
if (myBuildApkToRun) {
changeStatus(Status.BUILDING_APK);
if (!myHelper.buildApk()) {
changeStatus(Status.FAIL, ErrorCode.BUILD_APK_FAILED);
return;
}
changeStatus(Status.UPLOADING_APK);
if (!uploadApkIfNeeded()) {
changeStatus(Status.FAIL, ErrorCode.UPLOAD_APK_FAILED);
return;
}
changeStatus(Status.FETCHING_RESULT);
myPackageId = myApkHolder.getApk().getPackageId();
myFetchResponse =
myFetchAsGoogleClient
.fetchAsGoogle(myDeepLink, myPackageId, myApkHolder.getApk().getApkId(), RequestType.APP_INDEXING);
}
else {
// Will use apk from Play Store for test.
changeStatus(Status.FETCHING_RESULT);
myPackageId = myHelper.getPackageId();
if (StringUtil.isNotEmpty(myPackageId)) {
myFetchResponse = myFetchAsGoogleClient.fetchAsGoogle(myDeepLink, myPackageId, null, myRequestType);
Logger.getInstance(FetchAsGoogleTask.class).warn(myFetchResponse.getStatus());
}
else {
changeStatus(Status.FAIL, ErrorCode.FETCH_RESULT_FAILED);
return;
}
}
changeStatus(Status.SUCCESS);
}
catch (FetchAsGoogleException e) {
changeStatus(Status.FAIL, e.getErrorCode());
logger.warn(e);
}
catch (IOException ex) {
changeStatus(Status.FAIL, ErrorCode.NETWORK_ERROR);
logger.warn(ex);
}
catch (Exception exx) {
changeStatus(Status.FAIL, ErrorCode.UNKNOWN);
logger.warn(exx);
}
}
private boolean uploadApkIfNeeded() throws FetchAsGoogleException, IOException {
File apkFile = myHelper.getApkFile();
if (apkFile == null || !apkFile.exists()) {
return false;
}
String packageId = myHelper.getPackageId();
if (packageId == null || packageId.isEmpty()) {
return false;
}
byte[] apkHash = myUploadedApkManager.hashApk(apkFile);
if (apkHash == null) {
logger.warn("Couldn't get apk file hash");
return false;
}
myApkHolder = myUploadedApkManager.getApkHolder(apkHash);
if (myApkHolder == null) {
// Null means that the apk haven't been uploaded previously.
myApkHolder = myFetchAsGoogleClient.uploadApk(packageId, apkFile);
myUploadedApkManager.addUploadedApk(apkHash, myApkHolder);
}
return true;
}
/**
* An helper class which wrap some logic. So we can mock them in test case.
*/
static class Helper {
private Project myProject;
private Module myModule;
private static final Logger logger = Logger.getInstance(Helper.class);
private AssembleInvocationResult myAssembleResult;
public Helper(@NotNull Project project, @NotNull Module module) {
myProject = project;
myModule = module;
}
/**
* Builds the APK file, return whether the build is successful.
*/
public boolean buildApk() {
try {
final AtomicReference<String> errorMsgRef = new AtomicReference<>();
final GradleBuildInvoker gradleBuildInvoker = GradleBuildInvoker.getInstance(myProject);
ListenableFuture<AssembleInvocationResult> assembleResultFuture =
ActionsKt.invokeAndWaitIfNeeded(
ModalityState.NON_MODAL,
() -> gradleBuildInvoker.assemble(new Module[]{myModule}, TestCompileType.ALL));
myAssembleResult = assembleResultFuture.get();
return myAssembleResult.isBuildSuccessful();
}
catch (Throwable t) {
return false;
}
}
@Nullable
public File getApkFile() {
AndroidFacet facet = AndroidFacet.getInstance(myModule);
if (facet != null) {
List<ApkInfo> apkInfos = getBuiltApksForSelectedVariant(myAssembleResult, facet, false);
if (apkInfos != null && apkInfos.size() == 1 && apkInfos.get(0).getFiles().size() == 1) {
return apkInfos.get(0).getFiles().get(0).getApkFile();
}
}
return null;
}
@Nullable
public String getPackageId() {
final AtomicReference<String> packageId = new AtomicReference<>();
ApplicationManager.getApplication().invokeAndWait(() -> {
AndroidFacet androidFacet = AndroidFacet.getInstance(myModule);
if (androidFacet != null) {
AndroidModel androidModel = AndroidModel.get(androidFacet);
if (androidModel != null) {
packageId.set(androidModel.getApplicationId());
}
}
}, ModalityState.any());
return packageId.get();
}
}
}