blob: 5f75f5d42c2f96eedabf10c6314af30692cadf4c [file] [log] [blame]
/*
* Copyright (C) 2014 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.android.tools.idea.ddms.adb;
import com.android.annotations.concurrency.GuardedBy;
import com.android.ddmlib.*;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import org.jetbrains.android.actions.AndroidEnableAdbServiceAction;
import org.jetbrains.android.logcat.AdbErrors;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.concurrent.*;
import java.util.concurrent.TimeoutException;
/**
* {@link AdbService} is the main entry point to initializing and obtaining the {@link AndroidDebugBridge}.
*
* <p>Actions that require a handle to the debug bridge should invoke {@link #getDebugBridge(File)} to obtain the debug bridge.
* This bridge is only valid at the time it is obtained, and could go stale in the future (e.g. user disables
* adb integration via {@link AndroidEnableAdbServiceAction}, or launches monitor via
* {@link org.jetbrains.android.actions.AndroidRunDdmsAction}).
*
* <p>Components that need to keep a handle to the bridge for longer durations (such as tool windows that monitor device state) should do so
* by first invoking {@link #getDebugBridge(File)} to obtain the bridge, and implementing
* {@link AndroidDebugBridge.IDebugBridgeChangeListener} to ensure that they get updates to the status of the bridge.
*/
public class AdbService implements Disposable {
private static final Logger LOG = Logger.getInstance(AdbService.class);
@GuardedBy("this")
@Nullable private ListenableFuture<AndroidDebugBridge> myFuture;
/**
* adb initialization and termination could occur in separate threads (see {@link #terminateDdmlib()} and {@link CreateBridgeTask}.
* This lock is used to synchronize between the two.
* */
private static final Object ADB_INIT_LOCK = new Object();
public static AdbService getInstance() {
return ServiceManager.getService(AdbService.class);
}
private AdbService() {
DdmPreferences.setLogLevel(Log.LogLevel.INFO.getStringValue());
DdmPreferences.setTimeOut(AndroidUtils.TIMEOUT);
}
@Override
public void dispose() {
terminateDdmlib();
}
public synchronized ListenableFuture<AndroidDebugBridge> getDebugBridge(@NotNull File adb) {
// Cancel previous requests if they were unsuccessful
if (myFuture != null && myFuture.isDone() && !wasSuccessful(myFuture)) {
terminateDdmlib();
}
if (myFuture == null) {
Future<AndroidDebugBridge> future = ApplicationManager.getApplication().executeOnPooledThread(new CreateBridgeTask(adb));
// TODO: expose connection timeout in some settings UI? Also see AndroidUtils.TIMEOUT which is way too long
myFuture = makeTimedFuture(future, 20, TimeUnit.SECONDS);
}
return myFuture;
}
synchronized void terminateDdmlib() {
if (myFuture != null) {
myFuture.cancel(true);
myFuture = null;
}
synchronized (ADB_INIT_LOCK) {
AndroidDebugBridge.disconnectBridge();
AndroidDebugBridge.terminate();
}
}
public static boolean canDdmsBeCorrupted(@NotNull AndroidDebugBridge bridge) {
return isDdmsCorrupted(bridge) || allDevicesAreEmpty(bridge);
}
private static boolean allDevicesAreEmpty(@NotNull AndroidDebugBridge bridge) {
for (IDevice device : bridge.getDevices()) {
if (device.getClients().length > 0) {
return false;
}
}
return true;
}
public static boolean isDdmsCorrupted(@NotNull AndroidDebugBridge bridge) {
// TODO: find other way to check if debug service is available
IDevice[] devices = bridge.getDevices();
if (devices.length > 0) {
for (IDevice device : devices) {
Client[] clients = device.getClients();
if (clients.length > 0) {
ClientData clientData = clients[0].getClientData();
return clientData.getVmIdentifier() == null;
}
}
}
return false;
}
public synchronized ListenableFuture<AndroidDebugBridge> restartDdmlib(@NotNull Project project) {
terminateDdmlib();
File adb = AndroidSdkUtils.getAdb(project);
if (adb == null) {
throw new RuntimeException("Unable to locate Android SDK used by project: " + project.getName());
}
return getDebugBridge(adb);
}
/** Returns whether the future has completed successfully. */
private static boolean wasSuccessful(Future<AndroidDebugBridge> future) {
if (!future.isDone()) {
return false;
}
try {
AndroidDebugBridge bridge = future.get();
return bridge != null && bridge.isConnected();
}
catch (Exception e) {
return false;
}
}
private static class CreateBridgeTask implements Callable<AndroidDebugBridge> {
private final File myAdb;
public CreateBridgeTask(@NotNull File adb) {
myAdb = adb;
}
@Override
public AndroidDebugBridge call() throws Exception {
AdbErrors.clear();
boolean clientSupport = AndroidEnableAdbServiceAction.isAdbServiceEnabled();
LOG.info("Initializing adb using: " + myAdb.getAbsolutePath() + ", client support = " + clientSupport);
AndroidDebugBridge bridge;
synchronized (ADB_INIT_LOCK) {
AndroidDebugBridge.init(clientSupport);
bridge = AndroidDebugBridge.createBridge(myAdb.getPath(), false);
}
while (!bridge.isConnected()) {
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
// if cancelled, don't wait for connection and return immediately
return bridge;
}
}
LOG.info("Successfully connected to adb");
return bridge;
}
}
/** Returns a future that wraps the given future with a timeout. */
private static <T> ListenableFuture<T> makeTimedFuture(@NotNull final Future<T> delegate,
final long timeout,
@NotNull final TimeUnit unit) {
final SettableFuture<T> future = SettableFuture.create();
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
try {
T value = delegate.get(timeout, unit);
future.set(value);
}
catch (ExecutionException e) {
future.setException(e.getCause());
}
catch (InterruptedException e) {
delegate.cancel(true);
future.setException(e);
}
catch (TimeoutException e) {
delegate.cancel(true);
future.setException(e);
}
}
});
return future;
}
}