| /* |
| * 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(); |
| } |
| } |
| } |