blob: 8ae2e730ffd270eee7343ccee044e095a4314270 [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 com.android.annotations.VisibleForTesting;
import com.android.build.OutputFile;
import com.android.builder.model.AndroidArtifact;
import com.android.builder.model.AndroidArtifactOutput;
import com.android.tools.idea.gradle.project.build.invoker.GradleBuildInvoker;
import com.android.tools.idea.gradle.project.build.invoker.GradleInvocationResult;
import com.android.tools.idea.gradle.project.build.invoker.TestCompileType;
import com.android.tools.idea.gradle.project.model.AndroidModuleModel;
import com.android.tools.idea.gradle.project.sync.GradleSyncInvoker;
import com.android.tools.idea.gradle.project.sync.GradleSyncListener;
import com.android.tools.idea.gradle.project.sync.GradleSyncState;
import com.android.tools.idea.model.AndroidModel;
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.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 com.intellij.util.ThreeState;
import com.intellij.util.concurrency.Semaphore;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static com.google.wireless.android.sdk.stats.GradleSyncStats.Trigger.TRIGGER_USER_REQUEST;
/**
* 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
}
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,
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(new Runnable() {
@Override
public void run() {
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, RequestType.APP_INDEXING);
Logger.getInstance(FetchAsGoogleTask.class).warn(myFetchResponse.getStatus());
}
else {
changeStatus(Status.FAIL, ErrorCode.UPLOAD_APK_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);
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() {
// We reference the code in MakeBeforeRunTaskProvider#executeTask to write this method.
final AtomicBoolean success = new AtomicBoolean();
try {
final Semaphore done = new Semaphore();
done.down();
final AtomicReference<String> errorMsgRef = new AtomicReference<String>();
// If the model needs a sync, we need to sync "synchronously" before running.
// See: https://code.google.com/p/android/issues/detail?id=70718
GradleSyncState syncState = GradleSyncState.getInstance(myProject);
if (syncState.isSyncNeeded() != ThreeState.NO) {
GradleSyncInvoker.Request request = new GradleSyncInvoker.Request().setRunInBackground(false).setTrigger(
TRIGGER_USER_REQUEST);
GradleSyncInvoker.getInstance().requestProjectSync(myProject, request, new GradleSyncListener.Adapter() {
@Override
public void syncFailed(@NotNull Project project, @NotNull String errorMessage) {
errorMsgRef.set(errorMessage);
}
});
}
String errorMsg = errorMsgRef.get();
if (errorMsg != null) {
// Sync failed. There is no point on continuing, because most likely the model is either not there, or has stale information,
// including the path of the APK.
logger.warn("Unable to launch task. Project sync failed with message: " + errorMsg);
return false;
}
final GradleBuildInvoker gradleBuildInvoker = GradleBuildInvoker.getInstance(myProject);
final GradleBuildInvoker.AfterGradleInvocationTask afterTask = new GradleBuildInvoker.AfterGradleInvocationTask() {
@Override
public void execute(@NotNull GradleInvocationResult result) {
success.set(result.isBuildSuccessful());
gradleBuildInvoker.remove(this);
done.up();
}
};
if (myProject.isDisposed()) {
done.up();
}
else {
ApplicationManager.getApplication().invokeAndWait(() -> {
gradleBuildInvoker.add(afterTask);
gradleBuildInvoker.assemble(new Module[]{myModule}, TestCompileType.NONE);
}, ModalityState.NON_MODAL);
done.waitFor();
}
}
catch (Throwable t) {
return false;
}
return success.get();
}
@Nullable
public File getApkFile() {
File apkFile = null;
AndroidFacet facet = AndroidFacet.getInstance(myModule);
if (facet != null) {
AndroidModuleModel model = AndroidModuleModel.get(facet);
if (model != null) {
AndroidArtifact artifact = model.getMainArtifact();
for (AndroidArtifactOutput output : artifact.getOutputs()) {
OutputFile file = output.getMainOutputFile();
apkFile = file.getOutputFile();
}
}
}
return apkFile;
}
@Nullable
public String getPackageId() {
final AtomicReference<String> packageId = new AtomicReference<String>();
ApplicationManager.getApplication().invokeAndWait(() -> {
AndroidFacet androidFacet = AndroidFacet.getInstance(myModule);
if (androidFacet != null) {
AndroidModel androidModel = androidFacet.getAndroidModel();
if (androidModel != null) {
packageId.set(androidModel.getApplicationId());
}
}
}, ModalityState.any());
return packageId.get();
}
}
}