com.android.tools.idea.logcat.AndroidLogcatService.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.logcat.AndroidLogcatService.java

Source

/*
 * Copyright (C) 2016 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.logcat;

import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.logcat.LogCatHeader;
import com.android.ddmlib.logcat.LogCatMessage;
import com.android.ddmlib.logcat.LogCatTimestamp;
import com.android.tools.idea.run.LoggingReceiver;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.intellij.execution.impl.ConsoleBuffer;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/**
 * {@link AndroidLogcatService} is the class that manages logs in all connected devices and emulators.
 * Other classes can call {@link AndroidLogcatService#addListener(IDevice, LogcatListener)} to listen for logs of specific device/emulator.
 * Listeners invoked in a pooled thread and this class is thread safe.
 */
@ThreadSafe
public final class AndroidLogcatService implements AndroidDebugBridge.IDeviceChangeListener, Disposable {
    private static Logger getLog() {
        return Logger.getInstance(AndroidLogcatService.class);
    }

    private static class LogcatBuffer {
        private int myBufferSize;
        private final LinkedList<LogCatMessage> myMessages = new LinkedList<>();

        public void addMessage(@NotNull LogCatMessage message) {
            myMessages.add(message);
            myBufferSize += message.getMessage().length();
            if (ConsoleBuffer.useCycleBuffer()) {
                while (myBufferSize > ConsoleBuffer.getCycleBufferSize()) {
                    myBufferSize -= myMessages.removeFirst().getMessage().length();
                }
            }
        }

        @NotNull
        public List<LogCatMessage> getMessages() {
            return myMessages;
        }
    }

    public interface LogcatListener {
        default void onLogLineReceived(@NotNull LogCatMessage line) {
        }

        default void onCleared() {
        }
    }

    private final Object myLock = new Object();

    @GuardedBy("myLock")
    private final Map<IDevice, List<LogcatListener>> myListeners = new HashMap<>();

    @GuardedBy("myLock")
    private final Map<IDevice, LogcatBuffer> myLogBuffers = new HashMap<>();

    @GuardedBy("myLock")
    private final Map<IDevice, AndroidLogcatReceiver> myLogReceivers = new HashMap<>();

    /**
     * This is a list of commands to execute per device. We use a newSingleThreadExecutor
     * to model a single queue of tasks to run, but that is poorly reflected in the
     * type of the variable.
     */
    @GuardedBy("myLock")
    private final Map<IDevice, ExecutorService> myExecutors = new HashMap<>();

    @NotNull
    public static AndroidLogcatService getInstance() {
        return ServiceManager.getService(AndroidLogcatService.class);
    }

    @TestOnly
    AndroidLogcatService() {
        AndroidDebugBridge.addDeviceChangeListener(this);
    }

    private void startReceiving(@NotNull final IDevice device) {
        synchronized (myLock) {
            if (myLogReceivers.containsKey(device)) {
                return;
            }
            connect(device);
            final AndroidLogcatReceiver receiver = createReceiver(device);
            myLogReceivers.put(device, receiver);
            myLogBuffers.put(device, new LogcatBuffer());
            ExecutorService executor = myExecutors.get(device);
            executor.submit((() -> {
                try {
                    AndroidUtils.executeCommandOnDevice(device, "logcat -v long", receiver, true);
                } catch (Exception e) {
                    getLog().info(String.format(
                            "Caught exception when capturing logcat output from the device %1$s. Receiving logcat output from this device will be "
                                    + "stopped, and the listeners will be notified with this exception as the last message",
                            device.getName()), e);
                    LogCatHeader dummyHeader = new LogCatHeader(Log.LogLevel.ERROR, 0, 0, "?", "Internal",
                            LogCatTimestamp.ZERO);
                    receiver.notifyLine(dummyHeader, e.getMessage());
                }
            }));
        }
    }

    @NotNull
    private AndroidLogcatReceiver createReceiver(@NotNull final IDevice device) {
        final LogcatListener logcatListener = new LogcatListener() {
            @Override
            public void onLogLineReceived(@NotNull LogCatMessage line) {
                synchronized (myLock) {
                    if (myListeners.containsKey(device)) {
                        for (LogcatListener listener : myListeners.get(device)) {
                            listener.onLogLineReceived(line);
                        }
                    }
                    if (myLogBuffers.containsKey(device)) {
                        myLogBuffers.get(device).addMessage(line);
                    }
                }
            }
        };
        return new AndroidLogcatReceiver(device, logcatListener);
    }

    private void connect(@NotNull IDevice device) {
        synchronized (myLock) {
            if (!myExecutors.containsKey(device)) {
                ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("logcat-" + device.getName())
                        .build();
                myExecutors.put(device, Executors.newSingleThreadExecutor(factory));
            }
        }
    }

    private void disconnect(@NotNull IDevice device) {
        synchronized (myLock) {
            stopReceiving(device);
            myExecutors.remove(device);
        }
    }

    private void stopReceiving(@NotNull IDevice device) {
        synchronized (myLock) {
            if (myLogReceivers.containsKey(device)) {
                myLogReceivers.get(device).cancel();
                myLogReceivers.remove(device);
                myLogBuffers.remove(device);
            }
        }
    }

    /**
     * Clears logs for the current device.
     */
    public void clearLogcat(@NotNull IDevice device, @NotNull Project project) {
        // In theory, we only need to clear the buffer. However, due to issues in the platform, clearing logcat via "logcat -c" could
        // end up blocking the current logcat readers. As a result, we need to issue a restart of the logging to work around the platform bug.
        // See https://code.google.com/p/android/issues/detail?id=81164 and https://android-review.googlesource.com/#/c/119673
        // NOTE: We can avoid this and just clear the console if we ever decide to stop issuing a "logcat -c" to the device or if we are
        // confident that https://android-review.googlesource.com/#/c/119673 doesn't happen anymore.
        synchronized (myLock) {
            ExecutorService executor = myExecutors.get(device);
            // If someone keeps a reference to a device that is disconnected, executor will be null.
            if (executor != null) {
                stopReceiving(device);

                executor.submit(() -> {
                    try {
                        AndroidUtils.executeCommandOnDevice(device, "logcat -c", new LoggingReceiver(getLog()),
                                false);
                    } catch (final Exception e) {
                        getLog().info(e);
                        ApplicationManager.getApplication()
                                .invokeLater(() -> Messages.showErrorDialog(project, "Error: " + e.getMessage(),
                                        AndroidBundle.message("android.logcat.error.dialog.title")));
                    }

                    synchronized (myLock) {
                        if (myListeners.containsKey(device)) {
                            for (LogcatListener listener : myListeners.get(device)) {
                                listener.onCleared();
                            }
                        }
                    }
                });

                startReceiving(device);
            }
        }
    }

    /**
     * Add a listener which receives each line, unfiltered, that comes from the specified device. If {@code addOldLogs} is true,
     * this will also notify the listener of every log message received so far.
     * Multi-line messages will be parsed into single lines and sent with the same header.
     * For example, Log.d(tag, "Line1\nLine2") will be sent to listeners in two iterations,
     * first: "Line1" with a header, second: "Line2" with the same header.
     * Listeners are invoked in a pooled thread, and they are triggered A LOT. You should be very careful if delegating this text
     * to a UI thread. For example, don't directly invoke a runnable on the UI thread per line, but consider batching many log lines first.
     */
    public void addListener(@NotNull IDevice device, @NotNull LogcatListener listener, boolean addOldLogs) {
        synchronized (myLock) {
            if (addOldLogs && myLogBuffers.containsKey(device)) {
                for (LogCatMessage line : myLogBuffers.get(device).getMessages()) {
                    listener.onLogLineReceived(line);
                }
            }

            if (!myListeners.containsKey(device)) {
                myListeners.put(device, new ArrayList<>());
            }

            myListeners.get(device).add(listener);

            if (device.isOnline()) {
                startReceiving(device);
            }
        }
    }

    /**
     * @see #addListener(IDevice, LogcatListener, boolean)
     */
    public void addListener(@NotNull IDevice device, @NotNull LogcatListener listener) {
        addListener(device, listener, false);
    }

    public void removeListener(@NotNull IDevice device, @NotNull LogcatListener listener) {
        synchronized (myLock) {
            if (myListeners.containsKey(device)) {
                myListeners.get(device).remove(listener);

                if (myListeners.get(device).isEmpty()) {
                    stopReceiving(device);
                }
            }
        }
    }

    @Override
    public void deviceConnected(@NotNull IDevice device) {
        if (device.isOnline()) {
            // TODO Evaluate if we really need to start getting logs as soon as we connect, or whether a connect would suffice.
            startReceiving(device);
        }
    }

    @Override
    public void deviceDisconnected(@NotNull IDevice device) {
        disconnect(device);
    }

    @Override
    public void deviceChanged(@NotNull IDevice device, int changeMask) {
        if (device.isOnline()) {
            startReceiving(device);
        } else {
            disconnect(device);
        }
    }

    @Override
    public void dispose() {
        AndroidDebugBridge.removeDeviceChangeListener(this);
        synchronized (myLock) {
            for (AndroidLogcatReceiver receiver : myLogReceivers.values()) {
                receiver.cancel();
            }
        }
    }
}