/* * 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.server.telecom; import android.app.Notification; import android.app.NotificationManager; import android.app.Person; import android.content.Context; import android.os.VibrationEffect; import android.telecom.Log; import android.telecom.TelecomManager; import android.media.AudioAttributes; import android.media.AudioManager; import android.media.Ringtone; import android.media.VolumeShaper; import android.net.Uri; import android.os.Bundle; import android.os.Vibrator; import com.android.internal.annotations.VisibleForTesting; import com.android.server.telecom.LogUtils.EventTimer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; /** * Controls the ringtone player. */ @VisibleForTesting public class Ringer { public static class VibrationEffectProxy { public VibrationEffect createWaveform(long[] timings, int[] amplitudes, int repeat) { return VibrationEffect.createWaveform(timings, amplitudes, repeat); } public VibrationEffect get(Uri ringtoneUri, Context context) { return VibrationEffect.get(ringtoneUri, context); } } @VisibleForTesting public VibrationEffect mDefaultVibrationEffect; private static final long[] PULSE_PRIMING_PATTERN = {0,12,250,12,500}; // priming + interval private static final int[] PULSE_PRIMING_AMPLITUDE = {0,255,0,255,0}; // priming + interval // ease-in + peak + pause private static final long[] PULSE_RAMPING_PATTERN = { 50,50,50,50,50,50,50,50,50,50,50,50,50,50,300,1000}; // ease-in (min amplitude = 30%) + peak + pause private static final int[] PULSE_RAMPING_AMPLITUDE = { 77,77,78,79,81,84,87,93,101,114,133,162,205,255,255,0}; private static final long[] PULSE_PATTERN; private static final int[] PULSE_AMPLITUDE; private static final int RAMPING_RINGER_VIBRATION_DURATION = 5000; private static final int RAMPING_RINGER_DURATION = 10000; static { // construct complete pulse pattern PULSE_PATTERN = new long[PULSE_PRIMING_PATTERN.length + PULSE_RAMPING_PATTERN.length]; System.arraycopy( PULSE_PRIMING_PATTERN, 0, PULSE_PATTERN, 0, PULSE_PRIMING_PATTERN.length); System.arraycopy(PULSE_RAMPING_PATTERN, 0, PULSE_PATTERN, PULSE_PRIMING_PATTERN.length, PULSE_RAMPING_PATTERN.length); // construct complete pulse amplitude PULSE_AMPLITUDE = new int[PULSE_PRIMING_AMPLITUDE.length + PULSE_RAMPING_AMPLITUDE.length]; System.arraycopy( PULSE_PRIMING_AMPLITUDE, 0, PULSE_AMPLITUDE, 0, PULSE_PRIMING_AMPLITUDE.length); System.arraycopy(PULSE_RAMPING_AMPLITUDE, 0, PULSE_AMPLITUDE, PULSE_PRIMING_AMPLITUDE.length, PULSE_RAMPING_AMPLITUDE.length); } private static final long[] SIMPLE_VIBRATION_PATTERN = { 0, // No delay before starting 1000, // How long to vibrate 1000, // How long to wait before vibrating again }; private static final int[] SIMPLE_VIBRATION_AMPLITUDE = { 0, // No delay before starting 255, // Vibrate full amplitude 0, // No amplitude while waiting }; /** * Indicates that vibration should be repeated at element 5 in the {@link #PULSE_AMPLITUDE} and * {@link #PULSE_PATTERN} arrays. This means repetition will happen for the main ease-in/peak * pattern, but the priming + interval part will not be repeated. */ private static final int REPEAT_VIBRATION_AT = 5; private static final int REPEAT_SIMPLE_VIBRATION_AT = 1; private static final float EPSILON = 1e-6f; private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .build(); private static VibrationEffect mRampingRingerVibrationEffect; private static VolumeShaper.Configuration mVolumeShaperConfig; /** * Used to keep ordering of unanswered incoming calls. There can easily exist multiple incoming * calls and explicit ordering is useful for maintaining the proper state of the ringer. */ private final SystemSettingsUtil mSystemSettingsUtil; private final InCallTonePlayer.Factory mPlayerFactory; private final AsyncRingtonePlayer mRingtonePlayer; private final Context mContext; private final Vibrator mVibrator; private final InCallController mInCallController; private final VibrationEffectProxy mVibrationEffectProxy; private final boolean mIsHapticPlaybackSupportedByDevice; /** * For unit testing purposes only; when set, {@link #startRinging(Call, boolean)} will complete * the future provided by the test using {@link #setBlockOnRingingFuture(CompletableFuture)}. */ private CompletableFuture mBlockOnRingingFuture = null; private CompletableFuture mVibrateFuture = CompletableFuture.completedFuture(null); private InCallTonePlayer mCallWaitingPlayer; private RingtoneFactory mRingtoneFactory; /** * Call objects that are ringing, vibrating or call-waiting. These are used only for logging * purposes. */ private Call mRingingCall; private Call mVibratingCall; private Call mCallWaitingCall; /** * Used to track the status of {@link #mVibrator} in the case of simultaneous incoming calls. */ private boolean mIsVibrating = false; /** Initializes the Ringer. */ @VisibleForTesting public Ringer( InCallTonePlayer.Factory playerFactory, Context context, SystemSettingsUtil systemSettingsUtil, AsyncRingtonePlayer asyncRingtonePlayer, RingtoneFactory ringtoneFactory, Vibrator vibrator, VibrationEffectProxy vibrationEffectProxy, InCallController inCallController) { mSystemSettingsUtil = systemSettingsUtil; mPlayerFactory = playerFactory; mContext = context; // We don't rely on getSystemService(Context.VIBRATOR_SERVICE) to make sure this // vibrator object will be isolated from others. mVibrator = vibrator; mRingtonePlayer = asyncRingtonePlayer; mRingtoneFactory = ringtoneFactory; mInCallController = inCallController; mVibrationEffectProxy = vibrationEffectProxy; if (mContext.getResources().getBoolean(R.bool.use_simple_vibration_pattern)) { mDefaultVibrationEffect = mVibrationEffectProxy.createWaveform(SIMPLE_VIBRATION_PATTERN, SIMPLE_VIBRATION_AMPLITUDE, REPEAT_SIMPLE_VIBRATION_AT); } else { mDefaultVibrationEffect = mVibrationEffectProxy.createWaveform(PULSE_PATTERN, PULSE_AMPLITUDE, REPEAT_VIBRATION_AT); } mIsHapticPlaybackSupportedByDevice = mSystemSettingsUtil.isHapticPlaybackSupported(mContext); } @VisibleForTesting public void setBlockOnRingingFuture(CompletableFuture future) { mBlockOnRingingFuture = future; } public boolean startRinging(Call foregroundCall, boolean isHfpDeviceAttached) { if (foregroundCall == null) { Log.wtf(this, "startRinging called with null foreground call."); return false; } if (foregroundCall.getState() != CallState.RINGING && foregroundCall.getState() != CallState.SIMULATED_RINGING) { // Its possible for bluetooth to connect JUST as a call goes active, which would mean // the call would start ringing again. Log.i(this, "startRinging called for non-ringing foreground callid=%s", foregroundCall.getId()); return false; } AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); LogUtils.EventTimer timer = new EventTimer(); boolean isVolumeOverZero = audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0; timer.record("isVolumeOverZero"); boolean shouldRingForContact = shouldRingForContact(foregroundCall.getContactUri()); timer.record("shouldRingForContact"); boolean isRingtonePresent = !(mRingtoneFactory.getRingtone(foregroundCall) == null); timer.record("getRingtone"); boolean isSelfManaged = foregroundCall.isSelfManaged(); timer.record("isSelfManaged"); boolean isSilentRingingRequested = foregroundCall.isSilentRingingRequested(); timer.record("isSilentRingRequested"); boolean isRingerAudible = isVolumeOverZero && shouldRingForContact && isRingtonePresent; timer.record("isRingerAudible"); boolean hasExternalRinger = hasExternalRinger(foregroundCall); timer.record("hasExternalRinger"); // Don't do call waiting operations or vibration unless these are false. boolean isTheaterModeOn = mSystemSettingsUtil.isTheaterModeOn(mContext); timer.record("isTheaterModeOn"); boolean letDialerHandleRinging = mInCallController.doesConnectedDialerSupportRinging(); timer.record("letDialerHandleRinging"); Log.i(this, "startRinging timings: " + timer); boolean endEarly = isTheaterModeOn || letDialerHandleRinging || isSelfManaged || hasExternalRinger || isSilentRingingRequested; // Acquire audio focus under any of the following conditions: // 1. Should ring for contact and there's an HFP device attached // 2. Volume is over zero, we should ring for the contact, and there's a audible ringtone // present. // 3. The call is self-managed. boolean shouldAcquireAudioFocus = isRingerAudible || (isHfpDeviceAttached && shouldRingForContact) || isSelfManaged; if (endEarly) { if (letDialerHandleRinging) { Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "Dialer handles"); } if (isSilentRingingRequested) { Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "Silent ringing " + "requested"); } Log.i(this, "Ending early -- isTheaterModeOn=%s, letDialerHandleRinging=%s, " + "isSelfManaged=%s, hasExternalRinger=%s, silentRingingRequested=%s", isTheaterModeOn, letDialerHandleRinging, isSelfManaged, hasExternalRinger, isSilentRingingRequested); if (mBlockOnRingingFuture != null) { mBlockOnRingingFuture.complete(null); } return shouldAcquireAudioFocus; } stopCallWaiting(); VibrationEffect effect; CompletableFuture hapticsFuture = null; // Determine if the settings and DND mode indicate that the vibrator can be used right now. boolean isVibratorEnabled = isVibratorEnabled(mContext, foregroundCall); if (isRingerAudible) { mRingingCall = foregroundCall; Log.addEvent(foregroundCall, LogUtils.Events.START_RINGER); // Because we wait until a contact info query to complete before processing a // call (for the purposes of direct-to-voicemail), the information about custom // ringtones should be available by the time this code executes. We can safely // request the custom ringtone from the call and expect it to be current. if (mSystemSettingsUtil.applyRampingRinger(mContext)) { Log.i(this, "start ramping ringer."); if (mSystemSettingsUtil.enableAudioCoupledVibrationForRampingRinger()) { effect = getVibrationEffectForCall(mRingtoneFactory, foregroundCall); } else { effect = mDefaultVibrationEffect; } if (mVolumeShaperConfig == null) { float silencePoint = (float) (RAMPING_RINGER_VIBRATION_DURATION) / (float) (RAMPING_RINGER_VIBRATION_DURATION + RAMPING_RINGER_DURATION); mVolumeShaperConfig = new VolumeShaper.Configuration.Builder() .setDuration(RAMPING_RINGER_VIBRATION_DURATION + RAMPING_RINGER_DURATION) .setCurve(new float[] {0.f, silencePoint + EPSILON /*keep monotonicity*/, 1.f}, new float[] {0.f, 0.f, 1.f}) .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR) .build(); } hapticsFuture = mRingtonePlayer.play(mRingtoneFactory, foregroundCall, mVolumeShaperConfig, isVibratorEnabled); } else { // Ramping ringtone is not enabled. hapticsFuture = mRingtonePlayer.play(mRingtoneFactory, foregroundCall, null, isVibratorEnabled); effect = getVibrationEffectForCall(mRingtoneFactory, foregroundCall); } } else { String reason = String.format( "isVolumeOverZero=%s, shouldRingForContact=%s, isRingtonePresent=%s", isVolumeOverZero, shouldRingForContact, isRingtonePresent); Log.i(this, "startRinging: skipping because ringer would not be audible. " + reason); Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "Inaudible: " + reason); effect = mDefaultVibrationEffect; } if (hapticsFuture != null) { mVibrateFuture = hapticsFuture.thenAccept(isUsingAudioCoupledHaptics -> { if (!isUsingAudioCoupledHaptics || !mIsHapticPlaybackSupportedByDevice) { Log.i(this, "startRinging: fileHasHaptics=%b, hapticsSupported=%b", isUsingAudioCoupledHaptics, mIsHapticPlaybackSupportedByDevice); maybeStartVibration(foregroundCall, shouldRingForContact, effect, isVibratorEnabled, isRingerAudible); } else if (mSystemSettingsUtil.applyRampingRinger(mContext) && !mSystemSettingsUtil.enableAudioCoupledVibrationForRampingRinger()) { Log.i(this, "startRinging: apply ramping ringer vibration"); maybeStartVibration(foregroundCall, shouldRingForContact, effect, isVibratorEnabled, isRingerAudible); } else { Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION, "using audio-coupled haptics"); } }); if (mBlockOnRingingFuture != null) { mVibrateFuture.whenComplete((v, e) -> mBlockOnRingingFuture.complete(null)); } } else { if (mBlockOnRingingFuture != null) { mBlockOnRingingFuture.complete(null); } Log.w(this, "startRinging: No haptics future; fallback to default behavior"); maybeStartVibration(foregroundCall, shouldRingForContact, effect, isVibratorEnabled, isRingerAudible); } return shouldAcquireAudioFocus; } private void maybeStartVibration(Call foregroundCall, boolean shouldRingForContact, VibrationEffect effect, boolean isVibrationEnabled, boolean isRingerAudible) { if (isVibrationEnabled && !mIsVibrating && shouldRingForContact) { if (mSystemSettingsUtil.applyRampingRinger(mContext) && isRingerAudible) { Log.i(this, "start vibration for ramping ringer."); mIsVibrating = true; mVibrator.vibrate(effect, VIBRATION_ATTRIBUTES); } else { Log.i(this, "start normal vibration."); mIsVibrating = true; mVibrator.vibrate(effect, VIBRATION_ATTRIBUTES); } } else if (mIsVibrating) { Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION, "already vibrating"); } } private VibrationEffect getVibrationEffectForCall(RingtoneFactory factory, Call call) { VibrationEffect effect = null; Ringtone ringtone = factory.getRingtone(call); Uri ringtoneUri = ringtone != null ? ringtone.getUri() : null; if (ringtoneUri != null) { try { effect = mVibrationEffectProxy.get(ringtoneUri, mContext); } catch (IllegalArgumentException iae) { // Deep in the bowels of the VibrationEffect class it is possible for an // IllegalArgumentException to be thrown if there is an invalid URI specified in the // device config, or a content provider failure. Rather than crashing the Telecom // process we will just use the default vibration effect. Log.e(this, iae, "getVibrationEffectForCall: failed to get vibration effect"); effect = null; } } if (effect == null) { effect = mDefaultVibrationEffect; } return effect; } public void startCallWaiting(Call call) { startCallWaiting(call, null); } public void startCallWaiting(Call call, String reason) { if (mSystemSettingsUtil.isTheaterModeOn(mContext)) { return; } if (mInCallController.doesConnectedDialerSupportRinging()) { Log.addEvent(call, LogUtils.Events.SKIP_RINGING, "Dialer handles"); return; } if (call.isSelfManaged()) { Log.addEvent(call, LogUtils.Events.SKIP_RINGING, "Self-managed"); return; } Log.v(this, "Playing call-waiting tone."); stopRinging(); if (mCallWaitingPlayer == null) { Log.addEvent(call, LogUtils.Events.START_CALL_WAITING_TONE, reason); mCallWaitingCall = call; mCallWaitingPlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING); mCallWaitingPlayer.startTone(); } } public void stopRinging() { if (mRingingCall != null) { Log.addEvent(mRingingCall, LogUtils.Events.STOP_RINGER); mRingingCall = null; } mRingtonePlayer.stop(); // If we haven't started vibrating because we were waiting for the haptics info, cancel // it and don't vibrate at all. if (mVibrateFuture != null) { mVibrateFuture.cancel(true); } if (mIsVibrating) { Log.addEvent(mVibratingCall, LogUtils.Events.STOP_VIBRATOR); mVibrator.cancel(); mIsVibrating = false; mVibratingCall = null; } } public void stopCallWaiting() { Log.v(this, "stop call waiting."); if (mCallWaitingPlayer != null) { if (mCallWaitingCall != null) { Log.addEvent(mCallWaitingCall, LogUtils.Events.STOP_CALL_WAITING_TONE); mCallWaitingCall = null; } mCallWaitingPlayer.stopTone(); mCallWaitingPlayer = null; } } public boolean isRinging() { return mRingtonePlayer.isPlaying(); } private boolean shouldRingForContact(Uri contactUri) { final NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); final Bundle peopleExtras = new Bundle(); if (contactUri != null) { ArrayList personList = new ArrayList<>(); personList.add(new Person.Builder().setUri(contactUri.toString()).build()); peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList); } return manager.matchesCallFilter(peopleExtras); } private boolean hasExternalRinger(Call foregroundCall) { Bundle intentExtras = foregroundCall.getIntentExtras(); if (intentExtras != null) { return intentExtras.getBoolean(TelecomManager.EXTRA_CALL_EXTERNAL_RINGER, false); } else { return false; } } private boolean isVibratorEnabled(Context context, Call call) { AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); int ringerMode = audioManager.getRingerModeInternal(); boolean shouldVibrate; if (getVibrateWhenRinging(context)) { shouldVibrate = ringerMode != AudioManager.RINGER_MODE_SILENT; } else { shouldVibrate = ringerMode == AudioManager.RINGER_MODE_VIBRATE; } // Technically this should be in the calling method, but it seemed a little odd to pass // around a whole bunch of state just for logging purposes. if (shouldVibrate) { Log.addEvent(call, LogUtils.Events.START_VIBRATOR, "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, isVibrating=%b", mVibrator.hasVibrator(), mSystemSettingsUtil.canVibrateWhenRinging(context), ringerMode, mIsVibrating); } else { Log.addEvent(call, LogUtils.Events.SKIP_VIBRATION, "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, isVibrating=%b", mVibrator.hasVibrator(), mSystemSettingsUtil.canVibrateWhenRinging(context), ringerMode, mIsVibrating); } return shouldVibrate; } private boolean getVibrateWhenRinging(Context context) { if (!mVibrator.hasVibrator()) { return false; } return mSystemSettingsUtil.canVibrateWhenRinging(context) || mSystemSettingsUtil.applyRampingRinger(context); } }