chromium/media/midi/java/src/org/chromium/midi/MidiManagerAndroid.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.midi;

import android.content.Context;
import android.content.pm.PackageManager;
import android.media.midi.MidiDevice;
import android.media.midi.MidiDeviceInfo;
import android.media.midi.MidiManager;
import android.os.Handler;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/** A Java class implementing midi::MidiManagerAndroid functionality. */
@JNINamespace("midi")
class MidiManagerAndroid {
    private static final String TAG = "MidiManagerAndroid";

    /** Set true when this instance is successfully initialized. */
    private boolean mIsInitialized;

    /** The devices held by this manager. */
    private final List<MidiDeviceAndroid> mDevices = new ArrayList<>();

    /** The device information instances which are being initialized. */
    private final Set<MidiDeviceInfo> mPendingDevices = new HashSet<>();

    /** The underlying MidiManager. */
    private final MidiManager mManager;

    /** Callbacks will run on the message queue associated with this handler. */
    private final Handler mHandler;

    /** The associated midi::MidiDeviceAndroid instance. */
    private final long mNativeManagerPointer;

    /**
     * True is this object is stopped.
     * This is needed because MidiManagerAndroid functions are called from the IO thread but
     * callbacks are called on the UI thread (because the IO thread doesn't have a Looper). We need
     * to protect each native function call with a synchronized block that also checks this flag.
     */
    private boolean mStopped;

    /** Checks if Android MIDI is supported on the device. */
    @CalledByNative
    static boolean hasSystemFeatureMidi() {
        return ContextUtils.getApplicationContext()
                .getPackageManager()
                .hasSystemFeature(PackageManager.FEATURE_MIDI);
    }

    /**
     * A creation function called by C++.
     * @param nativeManagerPointer The native pointer to a midi::MidiManagerAndroid object.
     */
    @CalledByNative
    static MidiManagerAndroid create(long nativeManagerPointer) {
        return new MidiManagerAndroid(nativeManagerPointer);
    }

    /**
     * @param nativeManagerPointer The native pointer to a midi::MidiManagerAndroid object.
     */
    private MidiManagerAndroid(long nativeManagerPointer) {
        assert !ThreadUtils.runningOnUiThread();

        mManager =
                (MidiManager)
                        ContextUtils.getApplicationContext().getSystemService(Context.MIDI_SERVICE);
        mHandler = new Handler(ThreadUtils.getUiThreadLooper());
        mNativeManagerPointer = nativeManagerPointer;
    }

    void postOnInitializationFailed() {
        mHandler.post(
            new Runnable() {
                @Override
                public void run() {
                    synchronized (MidiManagerAndroid.this) {
                        if (mStopped) {
                            return;
                        }
                        MidiManagerAndroidJni.get()
                                .onInitializationFailed(mNativeManagerPointer);
                    }
                }
            });
    }

    /**
     * Initializes this object.
     * This function must be called right after creation.
     */
    @CalledByNative
    void initialize() {
        if (mManager == null) {
            postOnInitializationFailed();
            return;
        }
        try {
            mManager.registerDeviceCallback(
                new MidiManager.DeviceCallback() {
                    @Override
                    public void onDeviceAdded(MidiDeviceInfo device) {
                        MidiManagerAndroid.this.onDeviceAdded(device);
                    }

                    @Override
                    public void onDeviceRemoved(MidiDeviceInfo device) {
                        MidiManagerAndroid.this.onDeviceRemoved(device);
                    }
                },
                mHandler);
        } catch (Throwable t) {
            // android.media.midi.MidiManager.registerDeviceCallback
            // may throw RemoteException caused by
            // SecurityException("too many MIDI listeners ... ")
            Log.e(TAG, "registerDeviceCallback error", t);
            postOnInitializationFailed();
            return;
        }
        MidiDeviceInfo[] infos = mManager.getDevices();

        for (final MidiDeviceInfo info : infos) {
            mPendingDevices.add(info);
            openDevice(info);
        }
        mHandler.post(
                new Runnable() {
                    @Override
                    public void run() {
                        synchronized (MidiManagerAndroid.this) {
                            if (mStopped) {
                                return;
                            }
                            if (mPendingDevices.isEmpty() && !mIsInitialized) {
                                MidiManagerAndroidJni.get()
                                        .onInitialized(
                                                mNativeManagerPointer,
                                                mDevices.toArray(new MidiDeviceAndroid[0]));
                                mIsInitialized = true;
                            }
                        }
                    }
                });
    }

    /** Marks this object as stopped. */
    @CalledByNative
    synchronized void stop() {
        mStopped = true;
    }

    private void openDevice(final MidiDeviceInfo info) {
        mManager.openDevice(
                info,
                new MidiManager.OnDeviceOpenedListener() {
                    @Override
                    public void onDeviceOpened(MidiDevice device) {
                        MidiManagerAndroid.this.onDeviceOpened(device, info);
                    }
                },
                mHandler);
    }

    /**
     * Called when a midi device is attached.
     * @param info the attached device information.
     */
    private void onDeviceAdded(final MidiDeviceInfo info) {
        if (!mIsInitialized) {
            mPendingDevices.add(info);
        }
        openDevice(info);
    }

    /**
     * Called when a midi device is detached.
     * @param info the detached device information.
     */
    private synchronized void onDeviceRemoved(MidiDeviceInfo info) {
        if (mStopped) {
            return;
        }
        for (MidiDeviceAndroid device : mDevices) {
            if (device.isOpen() && device.getInfo().getId() == info.getId()) {
                device.close();
                MidiManagerAndroidJni.get().onDetached(mNativeManagerPointer, device);
            }
        }
    }

    private synchronized void onDeviceOpened(MidiDevice device, MidiDeviceInfo info) {
        if (mStopped) {
            return;
        }
        mPendingDevices.remove(info);
        if (device != null) {
            MidiDeviceAndroid xdevice = new MidiDeviceAndroid(device);
            mDevices.add(xdevice);
            if (mIsInitialized) {
                MidiManagerAndroidJni.get().onAttached(mNativeManagerPointer, xdevice);
            }
        }
        if (!mIsInitialized && mPendingDevices.isEmpty()) {
            MidiManagerAndroidJni.get()
                    .onInitialized(
                            mNativeManagerPointer, mDevices.toArray(new MidiDeviceAndroid[0]));
            mIsInitialized = true;
        }
    }

    @NativeMethods
    interface Natives {
        void onInitialized(long nativeMidiManagerAndroid, MidiDeviceAndroid[] devices);

        void onInitializationFailed(long nativeMidiManagerAndroid);

        void onAttached(long nativeMidiManagerAndroid, MidiDeviceAndroid device);

        void onDetached(long nativeMidiManagerAndroid, MidiDeviceAndroid device);
    }
}