/*
 * 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.android.dialer.app.voicemail;

import android.content.Context;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.telecom.CallAudioState;
import com.android.dialer.common.LogUtil;
import java.util.concurrent.RejectedExecutionException;

/** This class manages all audio changes for voicemail playback. */
public final class VoicemailAudioManager
    implements OnAudioFocusChangeListener, WiredHeadsetManager.Listener {

  private static final String TAG = "VoicemailAudioManager";

  public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL;

  private AudioManager audioManager;
  private VoicemailPlaybackPresenter voicemailPlaybackPresenter;
  private WiredHeadsetManager wiredHeadsetManager;
  private boolean wasSpeakerOn;
  private CallAudioState callAudioState;
  private boolean bluetoothScoEnabled;

  public VoicemailAudioManager(
      Context context, VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
    audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    this.voicemailPlaybackPresenter = voicemailPlaybackPresenter;
    wiredHeadsetManager = new WiredHeadsetManager(context);
    wiredHeadsetManager.setListener(this);

    callAudioState = getInitialAudioState();
    LogUtil.i(
        "VoicemailAudioManager.VoicemailAudioManager", "Initial audioState = " + callAudioState);
  }

  public void requestAudioFocus() {
    int result =
        audioManager.requestAudioFocus(
            this, PLAYBACK_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
    if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
      throw new RejectedExecutionException("Could not capture audio focus.");
    }
    updateBluetoothScoState(true);
  }

  public void abandonAudioFocus() {
    updateBluetoothScoState(false);
    audioManager.abandonAudioFocus(this);
  }

  @Override
  public void onAudioFocusChange(int focusChange) {
    LogUtil.d("VoicemailAudioManager.onAudioFocusChange", "focusChange=" + focusChange);
    voicemailPlaybackPresenter.onAudioFocusChange(focusChange == AudioManager.AUDIOFOCUS_GAIN);
  }

  @Override
  public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
    LogUtil.i(
        "VoicemailAudioManager.onWiredHeadsetPluggedInChanged",
        "wired headset was plugged in changed: " + oldIsPluggedIn + " -> " + newIsPluggedIn);

    if (oldIsPluggedIn == newIsPluggedIn) {
      return;
    }

    int newRoute = callAudioState.getRoute(); // start out with existing route
    if (newIsPluggedIn) {
      newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
    } else {
      voicemailPlaybackPresenter.pausePlayback();
      if (wasSpeakerOn) {
        newRoute = CallAudioState.ROUTE_SPEAKER;
      } else {
        newRoute = CallAudioState.ROUTE_EARPIECE;
      }
    }

    voicemailPlaybackPresenter.setSpeakerphoneOn(newRoute == CallAudioState.ROUTE_SPEAKER);

    // We need to call this every time even if we do not change the route because the supported
    // routes changed either to include or not include WIRED_HEADSET.
    setSystemAudioState(
        new CallAudioState(false /* muted */, newRoute, calculateSupportedRoutes()));
  }

  public void setSpeakerphoneOn(boolean on) {
    setAudioRoute(on ? CallAudioState.ROUTE_SPEAKER : CallAudioState.ROUTE_WIRED_OR_EARPIECE);
  }

  public boolean isWiredHeadsetPluggedIn() {
    return wiredHeadsetManager.isPluggedIn();
  }

  public void registerReceivers() {
    // Receivers is plural because we expect to add bluetooth support.
    wiredHeadsetManager.registerReceiver();
  }

  public void unregisterReceivers() {
    wiredHeadsetManager.unregisterReceiver();
  }

  /**
   * Bluetooth SCO (Synchronous Connection-Oriented) is the "phone" bluetooth audio. The system will
   * route to the bluetooth headset automatically if A2DP ("media") is available, but if the headset
   * only supports SCO then dialer must route it manually.
   */
  private void updateBluetoothScoState(boolean hasAudioFocus) {
    if (hasAudioFocus) {
      if (hasMediaAudioCapability()) {
        bluetoothScoEnabled = false;
      } else {
        bluetoothScoEnabled = true;
        LogUtil.i(
            "VoicemailAudioManager.updateBluetoothScoState",
            "bluetooth device doesn't support media, using SCO instead");
      }
    } else {
      bluetoothScoEnabled = false;
    }
    applyBluetoothScoState();
  }

  private void applyBluetoothScoState() {
    if (bluetoothScoEnabled) {
      audioManager.startBluetoothSco();
      // The doc for startBluetoothSco() states it could take seconds to establish the SCO
      // connection, so we should probably resume the playback after we've acquired SCO.
      // In practice the delay is unnoticeable so this is ignored for simplicity.
      audioManager.setBluetoothScoOn(true);
    } else {
      audioManager.setBluetoothScoOn(false);
      audioManager.stopBluetoothSco();
    }
  }

  private boolean hasMediaAudioCapability() {
    for (AudioDeviceInfo info : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
      if (info.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
        return true;
      }
    }
    return false;
  }

  /**
   * Change the audio route, for example from earpiece to speakerphone.
   *
   * @param route The new audio route to use. See {@link CallAudioState}.
   */
  void setAudioRoute(int route) {
    LogUtil.v(
        "VoicemailAudioManager.setAudioRoute",
        "route: " + CallAudioState.audioRouteToString(route));

    // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
    int newRoute = selectWiredOrEarpiece(route, callAudioState.getSupportedRouteMask());

    // If route is unsupported, do nothing.
    if ((callAudioState.getSupportedRouteMask() | newRoute) == 0) {
      LogUtil.w(
          "VoicemailAudioManager.setAudioRoute",
          "Asking to set to a route that is unsupported: " + newRoute);
      return;
    }

    // Remember the new speaker state so it can be restored when the user plugs and unplugs
    // a headset.
    wasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;
    setSystemAudioState(
        new CallAudioState(false /* muted */, newRoute, callAudioState.getSupportedRouteMask()));
  }

  private CallAudioState getInitialAudioState() {
    int supportedRouteMask = calculateSupportedRoutes();
    int route = selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask);
    return new CallAudioState(false /* muted */, route, supportedRouteMask);
  }

  private int calculateSupportedRoutes() {
    int routeMask = CallAudioState.ROUTE_SPEAKER;
    if (wiredHeadsetManager.isPluggedIn()) {
      routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
    } else {
      routeMask |= CallAudioState.ROUTE_EARPIECE;
    }
    return routeMask;
  }

  private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
    // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
    // ROUTE_WIRED_OR_EARPIECE so that callers don't have to make a call to check which is
    // supported before calling setAudioRoute.
    if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) {
      route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
      if (route == 0) {
        LogUtil.e(
            "VoicemailAudioManager.selectWiredOrEarpiece",
            "One of wired headset or earpiece should always be valid.");
        // assume earpiece in this case.
        route = CallAudioState.ROUTE_EARPIECE;
      }
    }
    return route;
  }

  private void setSystemAudioState(CallAudioState callAudioState) {
    CallAudioState oldAudioState = this.callAudioState;
    this.callAudioState = callAudioState;

    LogUtil.i(
        "VoicemailAudioManager.setSystemAudioState",
        "changing from " + oldAudioState + " to " + this.callAudioState);

    // Audio route.
    if (this.callAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
      turnOnSpeaker(true);
    } else if (this.callAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE
        || this.callAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) {
      // Just handle turning off the speaker, the system will handle switching between wired
      // headset and earpiece.
      turnOnSpeaker(false);
      // BluetoothSco is not handled by the system so it has to be reset.
      applyBluetoothScoState();
    }
  }

  private void turnOnSpeaker(boolean on) {
    if (audioManager.isSpeakerphoneOn() != on) {
      LogUtil.i("VoicemailAudioManager.turnOnSpeaker", "turning speaker phone on: " + on);
      audioManager.setSpeakerphoneOn(on);
    }
  }
}
