diff --git a/Android.bp b/Android.bp index d4a04cc43fe7..95847673103e 100644 --- a/Android.bp +++ b/Android.bp @@ -271,6 +271,7 @@ java_defaults { "core/java/android/os/IThermalService.aidl", "core/java/android/os/IUpdateLock.aidl", "core/java/android/os/IUserManager.aidl", + ":libvibrator_aidl", "core/java/android/os/IVibratorService.aidl", "core/java/android/os/storage/IStorageManager.aidl", "core/java/android/os/storage/IStorageEventListener.aidl", @@ -794,6 +795,14 @@ filegroup { ], } +filegroup { + name: "libvibrator_aidl", + srcs: [ + "core/java/android/os/IExternalVibrationController.aidl", + "core/java/android/os/IExternalVibratorService.aidl", + ], +} + java_library { name: "framework", defaults: ["framework-defaults"], diff --git a/core/java/android/os/ExternalVibration.aidl b/core/java/android/os/ExternalVibration.aidl new file mode 100644 index 000000000000..2629a401541d --- /dev/null +++ b/core/java/android/os/ExternalVibration.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2018 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 android.os; + +parcelable ExternalVibration cpp_header "vibrator/ExternalVibration.h"; diff --git a/core/java/android/os/ExternalVibration.java b/core/java/android/os/ExternalVibration.java new file mode 100644 index 000000000000..69ab1d94a9e2 --- /dev/null +++ b/core/java/android/os/ExternalVibration.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2018 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 android.os; + +import android.annotation.NonNull; +import android.media.AudioAttributes; +import android.util.Slog; + +import com.android.internal.util.Preconditions; + +/** + * An ExternalVibration represents an on-going vibration being controlled by something other than + * the core vibrator service. + * + * @hide + */ +public class ExternalVibration implements Parcelable { + private static final String TAG = "ExternalVibration"; + private int mUid; + @NonNull + private String mPkg; + @NonNull + private AudioAttributes mAttrs; + @NonNull + private IExternalVibrationController mController; + // A token used to maintain equality comparisons when passing objects across process + // boundaries. + @NonNull + private IBinder mToken; + + public ExternalVibration(int uid, @NonNull String pkg, @NonNull AudioAttributes attrs, + @NonNull IExternalVibrationController controller) { + mUid = uid; + mPkg = Preconditions.checkNotNull(pkg); + mAttrs = Preconditions.checkNotNull(attrs); + mController = Preconditions.checkNotNull(controller); + mToken = new Binder(); + } + + private ExternalVibration(Parcel in) { + mUid = in.readInt(); + mPkg = in.readString(); + mAttrs = readAudioAttributes(in); + mController = IExternalVibrationController.Stub.asInterface(in.readStrongBinder()); + mToken = in.readStrongBinder(); + } + + private AudioAttributes readAudioAttributes(Parcel in) { + int usage = in.readInt(); + int contentType = in.readInt(); + int capturePreset = in.readInt(); + int flags = in.readInt(); + AudioAttributes.Builder builder = new AudioAttributes.Builder(); + return builder.setUsage(usage) + .setContentType(contentType) + .setCapturePreset(capturePreset) + .setFlags(flags) + .build(); + } + + public int getUid() { + return mUid; + } + + public String getPackage() { + return mPkg; + } + + public AudioAttributes getAudioAttributes() { + return mAttrs; + } + + /** + * Mutes the external vibration if it's playing and unmuted. + * + * @return whether the muting operation was successful + */ + public boolean mute() { + try { + mController.mute(); + } catch (RemoteException e) { + Slog.wtf(TAG, "Failed to mute vibration stream: " + this, e); + return false; + } + return true; + } + + /** + * Unmutes the external vibration if it's playing and muted. + * + * @return whether the unmuting operation was successful + */ + public boolean unmute() { + try { + mController.unmute(); + } catch (RemoteException e) { + Slog.wtf(TAG, "Failed to unmute vibration stream: " + this, e); + return false; + } + return true; + } + + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof ExternalVibration)) { + return false; + } + ExternalVibration other = (ExternalVibration) o; + return mToken.equals(other.mToken); + } + + @Override + public String toString() { + return "ExternalVibration{" + + "uid=" + mUid + ", " + + "pkg=" + mPkg + ", " + + "attrs=" + mAttrs + ", " + + "controller=" + mController + + "token=" + mController + + "}"; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mUid); + out.writeString(mPkg); + writeAudioAttributes(mAttrs, out, flags); + out.writeParcelable(mAttrs, flags); + out.writeStrongBinder(mController.asBinder()); + out.writeStrongBinder(mToken); + } + + private static void writeAudioAttributes(AudioAttributes attrs, Parcel out, int flags) { + out.writeInt(attrs.getUsage()); + out.writeInt(attrs.getContentType()); + out.writeInt(attrs.getCapturePreset()); + out.writeInt(attrs.getAllFlags()); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ExternalVibration createFromParcel(Parcel in) { + return new ExternalVibration(in); + } + + @Override + public ExternalVibration[] newArray(int size) { + return new ExternalVibration[size]; + } + }; +} diff --git a/core/java/android/os/IExternalVibrationController.aidl b/core/java/android/os/IExternalVibrationController.aidl new file mode 100644 index 000000000000..56da8c7cd46c --- /dev/null +++ b/core/java/android/os/IExternalVibrationController.aidl @@ -0,0 +1,45 @@ +/* + * Copyright 2018, 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 android.os; + +/** + * {@hide} + */ + +interface IExternalVibrationController { + /** + * A method to ask a currently playing vibration to mute (i.e. not vibrate). + * + * This method is only valid from the time that + * {@link IExternalVibratorService#onExternalVibrationStart} returns until + * {@link IExternalVibratorService#onExternalVibrationStop} returns. + * + * @return whether the mute operation was successful + */ + boolean mute(); + + /** + * A method to ask a currently playing vibration to unmute (i.e. start vibrating). + * + * This method is only valid from the time that + * {@link IExternalVibratorService#onExternalVibrationStart} returns until + * {@link IExternalVibratorService#onExternalVibrationStop} returns. + * + * @return whether the unmute operation was successful + */ + boolean unmute(); +} diff --git a/core/java/android/os/IExternalVibratorService.aidl b/core/java/android/os/IExternalVibratorService.aidl new file mode 100644 index 000000000000..666171fcbcb3 --- /dev/null +++ b/core/java/android/os/IExternalVibratorService.aidl @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2018, 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 android.os; + +import android.os.ExternalVibration; + +/** + * The communication channel by which an external system that wants to control the system + * vibrator can notify the vibrator subsystem. + * + * Some vibrators can be driven via multiple paths (e.g. as an audio channel) in addition to + * the usual interface, but we typically only want one vibration at a time playing because they + * don't mix well. In order to synchronize the two places where vibration might be controlled, + * we provide this interface so the vibrator subsystem has a chance to: + * + * 1) Decide whether the current vibration should play based on the current system policy. + * 2) Stop any currently on-going vibrations. + * {@hide} + */ +interface IExternalVibratorService { + const int SCALE_MUTE = -100; + const int SCALE_VERY_LOW = -2; + const int SCALE_LOW = -1; + const int SCALE_NONE = 0; + const int SCALE_HIGH = 1; + const int SCALE_VERY_HIGH = 2; + + /** + * A method called by the external system to start a vibration. + * + * If this returns {@code SCALE_MUTE}, then the vibration should not play. If this + * returns any other scale level, then any currently playing vibration controlled by the + * requesting system must be muted and this vibration can begin playback. + * + * Note that the IExternalVibratorService implementation will not call mute on any currently + * playing external vibrations in order to avoid re-entrancy with the system on the other side. + * + * @param vibration An ExternalVibration + * + * @return {@code SCALE_MUTE} if the external vibration should not play, and any other scale + * level if it should. + */ + int onExternalVibrationStart(in ExternalVibration vib); + + /** + * A method called by the external system when a vibration no longer wants to play. + */ + void onExternalVibrationStop(in ExternalVibration vib); +} diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java index ea6d435c7ba5..f6e698ff7839 100644 --- a/services/core/java/com/android/server/VibratorService.java +++ b/services/core/java/com/android/server/VibratorService.java @@ -33,8 +33,10 @@ import android.media.AudioAttributes; import android.media.AudioManager; import android.os.BatteryStats; import android.os.Binder; +import android.os.ExternalVibration; import android.os.Handler; import android.os.IBinder; +import android.os.IExternalVibratorService; import android.os.IVibratorService; import android.os.PowerManager; import android.os.PowerManager.ServiceType; @@ -75,16 +77,18 @@ public class VibratorService extends IVibratorService.Stub private static final String TAG = "VibratorService"; private static final boolean DEBUG = false; private static final String SYSTEM_UI_PACKAGE = "com.android.systemui"; + private static final String EXTERNAL_VIBRATOR_SERVICE = "external_vibrator_service"; private static final long[] DOUBLE_CLICK_EFFECT_FALLBACK_TIMINGS = { 0, 30, 100, 30 }; - // Scale levels. Each level is defined as the delta between the current setting and the default - // intensity for that type of vibration (i.e. current - default). - private static final int SCALE_VERY_LOW = -2; - private static final int SCALE_LOW = -1; - private static final int SCALE_NONE = 0; - private static final int SCALE_HIGH = 1; - private static final int SCALE_VERY_HIGH = 2; + // Scale levels. Each level, except MUTE, is defined as the delta between the current setting + // and the default intensity for that type of vibration (i.e. current - default). + private static final int SCALE_MUTE = IExternalVibratorService.SCALE_MUTE; // -100 + private static final int SCALE_VERY_LOW = IExternalVibratorService.SCALE_VERY_LOW; // -2 + private static final int SCALE_LOW = IExternalVibratorService.SCALE_LOW; // -1 + private static final int SCALE_NONE = IExternalVibratorService.SCALE_NONE; // 0 + private static final int SCALE_HIGH = IExternalVibratorService.SCALE_HIGH; // 1 + private static final int SCALE_VERY_HIGH = IExternalVibratorService.SCALE_VERY_HIGH; // 2 // Gamma adjustments for scale levels. private static final float SCALE_VERY_LOW_GAMMA = 2.0f; @@ -111,6 +115,7 @@ public class VibratorService extends IVibratorService.Stub private final int mPreviousVibrationsLimit; private final boolean mAllowPriorityVibrationsInLowPowerMode; private final boolean mSupportsAmplitudeControl; + private final boolean mSupportsExternalControl; private final int mDefaultVibrationAmplitude; private final SparseArray mFallbackEffects; private final SparseArray mProcStatesCache = new SparseArray(); @@ -138,18 +143,20 @@ public class VibratorService extends IVibratorService.Stub @GuardedBy("mLock") private Vibration mCurrentVibration; private int mCurVibUid = -1; + private ExternalVibration mCurrentExternalVibration; + private boolean mVibratorUnderExternalControl; private boolean mLowPowerMode; private int mHapticFeedbackIntensity; private int mNotificationIntensity; private int mRingIntensity; - native static boolean vibratorExists(); - native static void vibratorInit(); - native static void vibratorOn(long milliseconds); - native static void vibratorOff(); - native static boolean vibratorSupportsAmplitudeControl(); - native static void vibratorSetAmplitude(int amplitude); - native static long vibratorPerformEffect(long effect, long strength); + static native boolean vibratorExists(); + static native void vibratorInit(); + static native void vibratorOn(long milliseconds); + static native void vibratorOff(); + static native boolean vibratorSupportsAmplitudeControl(); + static native void vibratorSetAmplitude(int amplitude); + static native long vibratorPerformEffect(long effect, long strength); static native boolean vibratorSupportsExternalControl(); static native void vibratorSetExternalControl(boolean enabled); @@ -218,6 +225,9 @@ public class VibratorService extends IVibratorService.Stub } public boolean isHapticFeedback() { + if (VibratorService.this.isHapticFeedback(usageHint)) { + return true; + } if (effect instanceof VibrationEffect.Prebaked) { VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect; switch (prebaked.getId()) { @@ -239,19 +249,11 @@ public class VibratorService extends IVibratorService.Stub } public boolean isNotification() { - switch (usageHint) { - case AudioAttributes.USAGE_NOTIFICATION: - case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: - case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT: - case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED: - return true; - default: - return false; - } + return VibratorService.this.isNotification(usageHint); } public boolean isRingtone() { - return usageHint == AudioAttributes.USAGE_NOTIFICATION_RINGTONE; + return VibratorService.this.isRingtone(usageHint); } public boolean isFromSystem() { @@ -332,6 +334,7 @@ public class VibratorService extends IVibratorService.Stub vibratorOff(); mSupportsAmplitudeControl = vibratorSupportsAmplitudeControl(); + mSupportsExternalControl = vibratorSupportsExternalControl(); mContext = context; PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); @@ -379,6 +382,8 @@ public class VibratorService extends IVibratorService.Stub mScaleLevels.put(SCALE_NONE, new ScaleLevel(SCALE_NONE_GAMMA)); mScaleLevels.put(SCALE_HIGH, new ScaleLevel(SCALE_HIGH_GAMMA)); mScaleLevels.put(SCALE_VERY_HIGH, new ScaleLevel(SCALE_VERY_HIGH_GAMMA)); + + ServiceManager.addService(EXTERNAL_VIBRATOR_SERVICE, new ExternalVibratorService()); } private VibrationEffect createEffectFromResource(int resId) { @@ -562,6 +567,16 @@ public class VibratorService extends IVibratorService.Stub } } + + // If something has external control of the vibrator, assume that it's more + // important for now. + if (mCurrentExternalVibration != null) { + if (DEBUG) { + Slog.d(TAG, "Ignoring incoming vibration for current external vibration"); + } + return; + } + // If the current vibration is repeating and the incoming one is non-repeating, // then ignore the non-repeating vibration. This is so that we don't cancel // vibrations that are meant to grab the attention of the user, like ringtones and @@ -648,6 +663,11 @@ public class VibratorService extends IVibratorService.Stub mThread.cancel(); mThread = null; } + if (mCurrentExternalVibration != null) { + mCurrentExternalVibration.mute(); + mCurrentExternalVibration = null; + setVibratorUnderExternalControl(false); + } doVibratorOff(); reportFinishVibrationLocked(); } finally { @@ -1095,6 +1115,26 @@ public class VibratorService extends IVibratorService.Stub } } + private static boolean isNotification(int usageHint) { + switch (usageHint) { + case AudioAttributes.USAGE_NOTIFICATION: + case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + return true; + default: + return false; + } + } + + private static boolean isRingtone(int usageHint) { + return usageHint == AudioAttributes.USAGE_NOTIFICATION_RINGTONE; + } + + private static boolean isHapticFeedback(int usageHint) { + return usageHint == AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; + } + private void noteVibratorOnLocked(int uid, long millis) { try { mBatteryStatsService.noteVibratorOn(uid, millis); @@ -1116,6 +1156,18 @@ public class VibratorService extends IVibratorService.Stub } } + private void setVibratorUnderExternalControl(boolean externalControl) { + if (DEBUG) { + if (externalControl) { + Slog.d(TAG, "Vibrator going under external control."); + } else { + Slog.d(TAG, "Taking back control of vibrator."); + } + } + mVibratorUnderExternalControl = externalControl; + vibratorSetExternalControl(externalControl); + } + private class VibrateThread extends Thread { private final VibrationEffect.Waveform mWaveform; private final int mUid; @@ -1290,6 +1342,13 @@ public class VibratorService extends IVibratorService.Stub } else { pw.println("null"); } + pw.print(" mCurrentExternalVibration="); + if (mCurrentExternalVibration != null) { + pw.println(mCurrentExternalVibration.toString()); + } else { + pw.println("null"); + } + pw.println(" mVibratorUnderExternalControl=" + mVibratorUnderExternalControl); pw.println(" mLowPowerMode=" + mLowPowerMode); pw.println(" mHapticFeedbackIntensity=" + mHapticFeedbackIntensity); pw.println(" mNotificationIntensity=" + mNotificationIntensity); @@ -1310,6 +1369,87 @@ public class VibratorService extends IVibratorService.Stub new VibratorShellCommand(this).exec(this, in, out, err, args, callback, resultReceiver); } + final class ExternalVibratorService extends IExternalVibratorService.Stub { + @Override + public int onExternalVibrationStart(ExternalVibration vib) { + if (!mSupportsExternalControl) { + return SCALE_MUTE; + } + if (ActivityManager.checkComponentPermission(android.Manifest.permission.VIBRATE, + vib.getUid(), -1 /*owningUid*/, true /*exported*/) + != PackageManager.PERMISSION_GRANTED) { + Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid() + + " tried to play externally controlled vibration" + + " without VIBRATE permission, ignoring."); + return SCALE_MUTE; + } + + final int scaleLevel; + synchronized (mLock) { + if (!vib.equals(mCurrentExternalVibration)) { + if (mCurrentExternalVibration == null) { + // If we're not under external control right now, then cancel any normal + // vibration that may be playing and ready the vibrator for external + // control. + doCancelVibrateLocked(); + setVibratorUnderExternalControl(true); + } + // At this point we either have an externally controlled vibration playing, or + // no vibration playing. Since the interface defines that only one externally + // controlled vibration can play at a time, by returning something other than + // SCALE_MUTE from this function we can be assured that if we are currently + // playing vibration, it will be muted in favor of the new vibration. + // + // Note that this doesn't support multiple concurrent external controls, as we + // would need to mute the old one still if it came from a different controller. + mCurrentExternalVibration = vib; + if (DEBUG) { + Slog.e(TAG, "Playing external vibration: " + vib); + } + } + final int usage = vib.getAudioAttributes().getUsage(); + final int defaultIntensity; + final int currentIntensity; + if (isRingtone(usage)) { + defaultIntensity = mVibrator.getDefaultRingVibrationIntensity(); + currentIntensity = mRingIntensity; + } else if (isNotification(usage)) { + defaultIntensity = mVibrator.getDefaultNotificationVibrationIntensity(); + currentIntensity = mNotificationIntensity; + } else if (isHapticFeedback(usage)) { + defaultIntensity = mVibrator.getDefaultHapticFeedbackIntensity(); + currentIntensity = mHapticFeedbackIntensity; + } else { + defaultIntensity = 0; + currentIntensity = 0; + } + scaleLevel = currentIntensity - defaultIntensity; + } + if (scaleLevel >= SCALE_VERY_LOW && scaleLevel <= SCALE_VERY_HIGH) { + return scaleLevel; + } else { + // Presumably we want to play this but something about our scaling has gone + // wrong, so just play with no scaling. + Slog.w(TAG, "Error in scaling calculations, ended up with invalid scale level " + + scaleLevel + " for vibration " + vib); + return SCALE_NONE; + } + } + + @Override + public void onExternalVibrationStop(ExternalVibration vib) { + synchronized (mLock) { + if (vib.equals(mCurrentExternalVibration)) { + mCurrentExternalVibration = null; + setVibratorUnderExternalControl(false); + if (DEBUG) { + Slog.e(TAG, "Stopping external vibration" + vib); + } + } + } + } + } + private final class VibratorShellCommand extends ShellCommand { private final IBinder mToken;