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