1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.SystemApi;
22 import android.content.Context;
23 import android.media.audiopolicy.AudioVolumeGroup;
24 import android.os.IBinder;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.os.RemoteException;
28 import android.os.ServiceManager;
29 import android.util.Log;
30 
31 import java.util.Objects;
32 
33 /**
34  * @hide
35  * A class to represent volume information.
36  * Can be used to represent volume associated with a stream type or {@link AudioVolumeGroup}.
37  * Volume index is optional when used to represent a category of volume.
38  * Volume ranges are supported too, making the representation of volume changes agnostic regarding
39  * the range of values that are supported (e.g. can be used to map BT A2DP absolute volume range to
40  * internal range).
41  */
42 @SystemApi
43 public final class VolumeInfo implements Parcelable {
44     private static final String TAG = "VolumeInfo";
45 
46     private final boolean mUsesStreamType; // false implies AudioVolumeGroup is used
47     private final boolean mHasMuteCommand;
48     private final boolean mIsMuted;
49     private final int mVolIndex;
50     private final int mMinVolIndex;
51     private final int mMaxVolIndex;
52     private final @Nullable AudioVolumeGroup mVolGroup;
53     private final @AudioManager.PublicStreamTypes int mStreamType;
54 
55     private static IAudioService sService;
56     private static VolumeInfo sDefaultVolumeInfo;
57 
VolumeInfo(boolean usesStreamType, boolean hasMuteCommand, boolean isMuted, int volIndex, int minVolIndex, int maxVolIndex, AudioVolumeGroup volGroup, int streamType)58     private VolumeInfo(boolean usesStreamType, boolean hasMuteCommand, boolean isMuted,
59             int volIndex, int minVolIndex, int maxVolIndex,
60             AudioVolumeGroup volGroup, int streamType) {
61         mUsesStreamType = usesStreamType;
62         mHasMuteCommand = hasMuteCommand;
63         mIsMuted = isMuted;
64         mVolIndex = volIndex;
65         mMinVolIndex = minVolIndex;
66         mMaxVolIndex = maxVolIndex;
67         mVolGroup = volGroup;
68         mStreamType = streamType;
69     }
70 
71     /**
72      * Indicates whether this instance has a stream type associated to it.
73      * Note this method returning true implies {@link #hasVolumeGroup()} returns false.
74      * (e.g. {@link AudioManager#STREAM_MUSIC}).
75      * @return true if it has stream type information
76      */
hasStreamType()77     public boolean hasStreamType() {
78         return mUsesStreamType;
79     }
80 
81     /**
82      * Returns the associated stream type, or will throw if {@link #hasStreamType()} returned false.
83      * @return a stream type value, see AudioManager.STREAM_*
84      * @throws IllegalStateException when called on a VolumeInfo not configured for
85      *      stream types.
86      */
getStreamType()87     public @AudioManager.PublicStreamTypes int getStreamType() {
88         if (!mUsesStreamType) {
89             throw new IllegalStateException("VolumeInfo doesn't use stream types");
90         }
91         return mStreamType;
92     }
93 
94     /**
95      * Indicates whether this instance has a {@link AudioVolumeGroup} associated to it.
96      * Note this method returning true implies {@link #hasStreamType()} returns false.
97      * @return true if it has volume group information
98      */
hasVolumeGroup()99     public boolean hasVolumeGroup() {
100         return !mUsesStreamType;
101     }
102 
103     /**
104      * Returns the associated volume group, or will throw if {@link #hasVolumeGroup()} returned
105      * false.
106      * @return the volume group corresponding to this VolumeInfo
107      * @throws IllegalStateException when called on a VolumeInfo not configured for
108      * volume groups.
109      */
getVolumeGroup()110     public @NonNull AudioVolumeGroup getVolumeGroup() {
111         if (mUsesStreamType) {
112             throw new IllegalStateException("VolumeInfo doesn't use AudioVolumeGroup");
113         }
114         return mVolGroup;
115     }
116 
117     /**
118      * Return whether this instance is conveying a mute state
119      * @return true if the muted state was explicitly set for this instance
120      */
hasMuteCommand()121     public boolean hasMuteCommand() {
122         return mHasMuteCommand;
123     }
124 
125     /**
126      * Returns whether this instance is conveying a mute state that was explicitly set
127      * by {@link Builder#setMuted(boolean)}, false otherwise
128      * @return true if the volume state is muted
129      */
isMuted()130     public boolean isMuted() {
131         return mIsMuted;
132     }
133 
134     /**
135      * A value used to express no volume index has been set.
136      */
137     public static final int INDEX_NOT_SET = -100;
138 
139     /**
140      * Returns the volume index.
141      * @return a volume index, or {@link #INDEX_NOT_SET} if no index was set, in which case this
142      *      instance is used to express a volume representation type (stream vs group) and
143      *      optionally its volume range
144      */
getVolumeIndex()145     public int getVolumeIndex() {
146         return mVolIndex;
147     }
148 
149     /**
150      * Returns the minimum volume index.
151      * @return the minimum volume index, or {@link #INDEX_NOT_SET} if no minimum index was set.
152      */
getMinVolumeIndex()153     public int getMinVolumeIndex() {
154         return mMinVolIndex;
155     }
156 
157     /**
158      * Returns the maximum volume index.
159      * @return the maximum volume index, or {@link #INDEX_NOT_SET} if no maximum index was
160      *      set.
161      */
getMaxVolumeIndex()162     public int getMaxVolumeIndex() {
163         return mMaxVolIndex;
164     }
165 
166     /**
167      * Returns the default info for the platform, typically initialized
168      * to STREAM_MUSIC with min/max initialized to the associated range
169      * @return the default VolumeInfo for the device
170      */
getDefaultVolumeInfo()171     public static @NonNull VolumeInfo getDefaultVolumeInfo() {
172         if (sService == null) {
173             IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
174             sService = IAudioService.Stub.asInterface(b);
175         }
176         if (sDefaultVolumeInfo == null) {
177             try {
178                 sDefaultVolumeInfo = sService.getDefaultVolumeInfo();
179             } catch (RemoteException e) {
180                 Log.e(TAG, "Error calling getDefaultVolumeInfo", e);
181                 // return a valid value, but don't cache it
182                 return new VolumeInfo.Builder(AudioManager.STREAM_MUSIC).build();
183             }
184         }
185         return sDefaultVolumeInfo;
186     }
187 
188     /**
189      * The builder class for creating and initializing, or copying and modifying VolumeInfo
190      * instances
191      */
192     public static final class Builder {
193         private boolean mUsesStreamType = true; // false implies AudioVolumeGroup is used
194         private @AudioManager.PublicStreamTypes int mStreamType = AudioManager.STREAM_MUSIC;
195         private boolean mHasMuteCommand = false;
196         private boolean mIsMuted = false;
197         private int mVolIndex = INDEX_NOT_SET;
198         private int mMinVolIndex = INDEX_NOT_SET;
199         private int mMaxVolIndex = INDEX_NOT_SET;
200         private @Nullable AudioVolumeGroup mVolGroup;
201 
202         /**
203          * Builder constructor for stream type-based VolumeInfo
204          */
Builder(@udioManager.PublicStreamTypes int streamType)205         public Builder(@AudioManager.PublicStreamTypes int streamType) {
206             if (!AudioManager.isPublicStreamType(streamType)) {
207                 throw new IllegalArgumentException("Not a valid public stream type " + streamType);
208             }
209             mUsesStreamType = true;
210             mStreamType = streamType;
211         }
212 
213         /**
214          * Builder constructor for volume group-based VolumeInfo
215          */
Builder(@onNull AudioVolumeGroup volGroup)216         public Builder(@NonNull AudioVolumeGroup volGroup) {
217             Objects.requireNonNull(volGroup);
218             mUsesStreamType = false;
219             mStreamType = -Integer.MIN_VALUE;
220             mVolGroup = volGroup;
221         }
222 
223         /**
224          * Builder constructor to copy a given VolumeInfo.
225          * Note you can't change the stream type or volume group later.
226          */
Builder(@onNull VolumeInfo info)227         public Builder(@NonNull VolumeInfo info) {
228             Objects.requireNonNull(info);
229             mUsesStreamType = info.mUsesStreamType;
230             mStreamType = info.mStreamType;
231             mHasMuteCommand = info.mHasMuteCommand;
232             mIsMuted = info.mIsMuted;
233             mVolIndex = info.mVolIndex;
234             mMinVolIndex = info.mMinVolIndex;
235             mMaxVolIndex = info.mMaxVolIndex;
236             mVolGroup = info.mVolGroup;
237         }
238 
239         /**
240          * Sets whether the volume is in a muted state
241          * @param isMuted
242          * @return the same builder instance
243          */
setMuted(boolean isMuted)244         public @NonNull Builder setMuted(boolean isMuted) {
245             mHasMuteCommand = true;
246             mIsMuted = isMuted;
247             return this;
248         }
249 
250         /**
251          * Sets the volume index
252          * @param volIndex a 0 or greater value, or {@link #INDEX_NOT_SET} if unknown
253          * @return the same builder instance
254          */
setVolumeIndex(int volIndex)255         public @NonNull Builder setVolumeIndex(int volIndex) {
256             if (volIndex != INDEX_NOT_SET && volIndex < 0) {
257                 throw new IllegalArgumentException("Volume index cannot be negative");
258             }
259             mVolIndex = volIndex;
260             return this;
261         }
262 
263         /**
264          * Sets the minimum volume index
265          * @param minIndex a 0 or greater value, or {@link #INDEX_NOT_SET} if unknown
266          * @return the same builder instance
267          */
setMinVolumeIndex(int minIndex)268         public @NonNull Builder setMinVolumeIndex(int minIndex) {
269             if (minIndex != INDEX_NOT_SET && minIndex < 0) {
270                 throw new IllegalArgumentException("Min volume index cannot be negative");
271             }
272             mMinVolIndex = minIndex;
273             return this;
274         }
275 
276         /**
277          * Sets the maximum volume index
278          * @param maxIndex a 0 or greater value, or {@link #INDEX_NOT_SET} if unknown
279          * @return the same builder instance
280          */
setMaxVolumeIndex(int maxIndex)281         public @NonNull Builder setMaxVolumeIndex(int maxIndex) {
282             if (maxIndex != INDEX_NOT_SET && maxIndex < 0) {
283                 throw new IllegalArgumentException("Max volume index cannot be negative");
284             }
285             mMaxVolIndex = maxIndex;
286             return this;
287         }
288 
289         /**
290          * Builds the VolumeInfo with the data given to the builder
291          * @return the new VolumeInfo instance
292          */
build()293         public @NonNull VolumeInfo build() {
294             if (mVolIndex != INDEX_NOT_SET) {
295                 if (mMinVolIndex != INDEX_NOT_SET && mVolIndex < mMinVolIndex) {
296                     throw new IllegalArgumentException("Volume index:" + mVolIndex
297                             + " lower than min index:" + mMinVolIndex);
298                 }
299                 if (mMaxVolIndex != INDEX_NOT_SET && mVolIndex > mMaxVolIndex) {
300                     throw new IllegalArgumentException("Volume index:" + mVolIndex
301                             + " greater than max index:" + mMaxVolIndex);
302                 }
303             }
304             if (mMinVolIndex != INDEX_NOT_SET && mMaxVolIndex != INDEX_NOT_SET
305                     && mMinVolIndex > mMaxVolIndex) {
306                 throw new IllegalArgumentException("Min volume index:" + mMinVolIndex
307                         + " greater than max index:" + mMaxVolIndex);
308             }
309             return new VolumeInfo(mUsesStreamType, mHasMuteCommand, mIsMuted,
310                     mVolIndex, mMinVolIndex, mMaxVolIndex,
311                     mVolGroup, mStreamType);
312         }
313     }
314 
315     //-----------------------------------------------
316     // Parcelable
317     @Override
hashCode()318     public int hashCode() {
319         return Objects.hash(mUsesStreamType, mHasMuteCommand, mStreamType, mIsMuted,
320                 mVolIndex, mMinVolIndex, mMaxVolIndex, mVolGroup);
321     }
322 
323     @Override
equals(Object o)324     public boolean equals(Object o) {
325         if (this == o) return true;
326         if (o == null || getClass() != o.getClass()) return false;
327 
328         VolumeInfo that = (VolumeInfo) o;
329         return ((mUsesStreamType == that.mUsesStreamType)
330                 && (mStreamType == that.mStreamType)
331                 && (mHasMuteCommand == that.mHasMuteCommand)
332                 && (mIsMuted == that.mIsMuted)
333                 && (mVolIndex == that.mVolIndex)
334                 && (mMinVolIndex == that.mMinVolIndex)
335                 && (mMaxVolIndex == that.mMaxVolIndex)
336                 && Objects.equals(mVolGroup, that.mVolGroup));
337     }
338 
339     @Override
toString()340     public String toString() {
341         return new String("VolumeInfo:"
342                 + (mUsesStreamType ? (" streamType:" + mStreamType)
343                     : (" volGroup:" + mVolGroup))
344                 + (mHasMuteCommand ? (" muted:" + mIsMuted) : ("[no mute cmd]"))
345                 + ((mVolIndex != INDEX_NOT_SET) ? (" volIndex:" + mVolIndex) : "")
346                 + ((mMinVolIndex != INDEX_NOT_SET) ? (" min:" + mMinVolIndex) : "")
347                 + ((mMaxVolIndex != INDEX_NOT_SET) ? (" max:" + mMaxVolIndex) : ""));
348     }
349 
350     @Override
describeContents()351     public int describeContents() {
352         return 0;
353     }
354 
355     @Override
writeToParcel(@onNull Parcel dest, int flags)356     public void writeToParcel(@NonNull Parcel dest, int flags) {
357         dest.writeBoolean(mUsesStreamType);
358         dest.writeInt(mStreamType);
359         dest.writeBoolean(mHasMuteCommand);
360         dest.writeBoolean(mIsMuted);
361         dest.writeInt(mVolIndex);
362         dest.writeInt(mMinVolIndex);
363         dest.writeInt(mMaxVolIndex);
364         if (!mUsesStreamType) {
365             mVolGroup.writeToParcel(dest, 0 /*ignored*/);
366         }
367     }
368 
VolumeInfo(@onNull Parcel in)369     private VolumeInfo(@NonNull Parcel in) {
370         mUsesStreamType = in.readBoolean();
371         mStreamType = in.readInt();
372         mHasMuteCommand = in.readBoolean();
373         mIsMuted = in.readBoolean();
374         mVolIndex = in.readInt();
375         mMinVolIndex = in.readInt();
376         mMaxVolIndex = in.readInt();
377         if (!mUsesStreamType) {
378             mVolGroup = AudioVolumeGroup.CREATOR.createFromParcel(in);
379         } else {
380             mVolGroup = null;
381         }
382     }
383 
384     public static final @NonNull Parcelable.Creator<VolumeInfo> CREATOR =
385             new Parcelable.Creator<VolumeInfo>() {
386                 /**
387                  * Rebuilds a VolumeInfo previously stored with writeToParcel().
388                  * @param p Parcel object to read the VolumeInfo from
389                  * @return a new VolumeInfo created from the data in the parcel
390                  */
391                 public VolumeInfo createFromParcel(Parcel p) {
392                     return new VolumeInfo(p);
393                 }
394 
395                 public VolumeInfo[] newArray(int size) {
396                     return new VolumeInfo[size];
397                 }
398             };
399 }
400