1/*
2 * Copyright (C) 2014 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
17package android.telecom;
18
19import android.annotation.IntDef;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.annotation.TestApi;
23import android.bluetooth.BluetoothDevice;
24import android.os.Parcel;
25import android.os.Parcelable;
26
27import java.lang.annotation.Retention;
28import java.lang.annotation.RetentionPolicy;
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.Collection;
32import java.util.Collections;
33import java.util.List;
34import java.util.Locale;
35import java.util.Objects;
36import java.util.stream.Collectors;
37
38/**
39 *  Encapsulates the telecom audio state, including the current audio routing, supported audio
40 *  routing and mute.
41 */
42public final class CallAudioState implements Parcelable {
43    /** @hide */
44    @Retention(RetentionPolicy.SOURCE)
45    @IntDef(value={ROUTE_EARPIECE, ROUTE_BLUETOOTH, ROUTE_WIRED_HEADSET, ROUTE_SPEAKER},
46            flag=true)
47    public @interface CallAudioRoute {}
48
49    /** Direct the audio stream through the device's earpiece. */
50    public static final int ROUTE_EARPIECE      = 0x00000001;
51
52    /** Direct the audio stream through Bluetooth. */
53    public static final int ROUTE_BLUETOOTH     = 0x00000002;
54
55    /** Direct the audio stream through a wired headset. */
56    public static final int ROUTE_WIRED_HEADSET = 0x00000004;
57
58    /** Direct the audio stream through the device's speakerphone. */
59    public static final int ROUTE_SPEAKER       = 0x00000008;
60
61    /**
62     * Direct the audio stream through the device's earpiece or wired headset if one is
63     * connected.
64     */
65    public static final int ROUTE_WIRED_OR_EARPIECE = ROUTE_EARPIECE | ROUTE_WIRED_HEADSET;
66
67    /**
68     * Bit mask of all possible audio routes.
69     *
70     * @hide
71     **/
72    public static final int ROUTE_ALL = ROUTE_EARPIECE | ROUTE_BLUETOOTH | ROUTE_WIRED_HEADSET |
73            ROUTE_SPEAKER;
74
75    private final boolean isMuted;
76    private final int route;
77    private final int supportedRouteMask;
78    private final BluetoothDevice activeBluetoothDevice;
79    private final Collection<BluetoothDevice> supportedBluetoothDevices;
80
81    /**
82     * Constructor for a {@link CallAudioState} object.
83     *
84     * @param muted {@code true} if the call is muted, {@code false} otherwise.
85     * @param route The current audio route being used.
86     * Allowed values:
87     * {@link #ROUTE_EARPIECE}
88     * {@link #ROUTE_BLUETOOTH}
89     * {@link #ROUTE_WIRED_HEADSET}
90     * {@link #ROUTE_SPEAKER}
91     * @param supportedRouteMask Bit mask of all routes supported by this call. This should be a
92     * bitwise combination of the following values:
93     * {@link #ROUTE_EARPIECE}
94     * {@link #ROUTE_BLUETOOTH}
95     * {@link #ROUTE_WIRED_HEADSET}
96     * {@link #ROUTE_SPEAKER}
97     */
98    public CallAudioState(boolean muted, @CallAudioRoute int route,
99            @CallAudioRoute int supportedRouteMask) {
100        this(muted, route, supportedRouteMask, null, Collections.emptyList());
101    }
102
103    /** @hide */
104    @TestApi
105    public CallAudioState(boolean isMuted, @CallAudioRoute int route,
106            @CallAudioRoute int supportedRouteMask,
107            @Nullable BluetoothDevice activeBluetoothDevice,
108            @NonNull Collection<BluetoothDevice> supportedBluetoothDevices) {
109        this.isMuted = isMuted;
110        this.route = route;
111        this.supportedRouteMask = supportedRouteMask;
112        this.activeBluetoothDevice = activeBluetoothDevice;
113        this.supportedBluetoothDevices = supportedBluetoothDevices;
114    }
115
116    /** @hide */
117    public CallAudioState(CallAudioState state) {
118        isMuted = state.isMuted();
119        route = state.getRoute();
120        supportedRouteMask = state.getSupportedRouteMask();
121        activeBluetoothDevice = state.activeBluetoothDevice;
122        supportedBluetoothDevices = state.getSupportedBluetoothDevices();
123    }
124
125    /** @hide */
126    @SuppressWarnings("deprecation")
127    public CallAudioState(AudioState state) {
128        isMuted = state.isMuted();
129        route = state.getRoute();
130        supportedRouteMask = state.getSupportedRouteMask();
131        activeBluetoothDevice = null;
132        supportedBluetoothDevices = Collections.emptyList();
133    }
134
135    @Override
136    public boolean equals(Object obj) {
137        if (obj == null) {
138            return false;
139        }
140        if (!(obj instanceof CallAudioState)) {
141            return false;
142        }
143        CallAudioState state = (CallAudioState) obj;
144        if (supportedBluetoothDevices.size() != state.supportedBluetoothDevices.size()) {
145            return false;
146        }
147        for (BluetoothDevice device : supportedBluetoothDevices) {
148            if (!state.supportedBluetoothDevices.contains(device)) {
149                return false;
150            }
151        }
152        return Objects.equals(activeBluetoothDevice, state.activeBluetoothDevice) && isMuted() ==
153                state.isMuted() && getRoute() == state.getRoute() && getSupportedRouteMask() ==
154                state.getSupportedRouteMask();
155    }
156
157    @Override
158    public String toString() {
159        String bluetoothDeviceList = supportedBluetoothDevices.stream()
160                .map(BluetoothDevice::getAddress).collect(Collectors.joining(", "));
161
162        return String.format(Locale.US,
163                "[AudioState isMuted: %b, route: %s, supportedRouteMask: %s, " +
164                        "activeBluetoothDevice: [%s], supportedBluetoothDevices: [%s]]",
165                isMuted,
166                audioRouteToString(route),
167                audioRouteToString(supportedRouteMask),
168                activeBluetoothDevice,
169                bluetoothDeviceList);
170    }
171
172    /**
173     * @return {@code true} if the call is muted, {@code false} otherwise.
174     */
175    public boolean isMuted() {
176        return isMuted;
177    }
178
179    /**
180     * @return The current audio route being used.
181     */
182    @CallAudioRoute
183    public int getRoute() {
184        return route;
185    }
186
187    /**
188     * @return Bit mask of all routes supported by this call.
189     */
190    @CallAudioRoute
191    public int getSupportedRouteMask() {
192        return supportedRouteMask;
193    }
194
195    /**
196     * @return The {@link BluetoothDevice} through which audio is being routed.
197     *         Will not be {@code null} if {@link #getRoute()} returns {@link #ROUTE_BLUETOOTH}.
198     */
199    public BluetoothDevice getActiveBluetoothDevice() {
200        return activeBluetoothDevice;
201    }
202
203    /**
204     * @return {@link List} of {@link BluetoothDevice}s that can be used for this call.
205     */
206    public Collection<BluetoothDevice> getSupportedBluetoothDevices() {
207        return supportedBluetoothDevices;
208    }
209
210    /**
211     * Converts the provided audio route into a human readable string representation.
212     *
213     * @param route to convert into a string.
214     *
215     * @return String representation of the provided audio route.
216     */
217    public static String audioRouteToString(int route) {
218        if (route == 0 || (route & ~ROUTE_ALL) != 0x0) {
219            return "UNKNOWN";
220        }
221
222        StringBuffer buffer = new StringBuffer();
223        if ((route & ROUTE_EARPIECE) == ROUTE_EARPIECE) {
224            listAppend(buffer, "EARPIECE");
225        }
226        if ((route & ROUTE_BLUETOOTH) == ROUTE_BLUETOOTH) {
227            listAppend(buffer, "BLUETOOTH");
228        }
229        if ((route & ROUTE_WIRED_HEADSET) == ROUTE_WIRED_HEADSET) {
230            listAppend(buffer, "WIRED_HEADSET");
231        }
232        if ((route & ROUTE_SPEAKER) == ROUTE_SPEAKER) {
233            listAppend(buffer, "SPEAKER");
234        }
235
236        return buffer.toString();
237    }
238
239    /**
240     * Responsible for creating AudioState objects for deserialized Parcels.
241     */
242    public static final Parcelable.Creator<CallAudioState> CREATOR =
243            new Parcelable.Creator<CallAudioState> () {
244
245        @Override
246        public CallAudioState createFromParcel(Parcel source) {
247            boolean isMuted = source.readByte() == 0 ? false : true;
248            int route = source.readInt();
249            int supportedRouteMask = source.readInt();
250            BluetoothDevice activeBluetoothDevice = source.readParcelable(
251                    ClassLoader.getSystemClassLoader());
252            List<BluetoothDevice> supportedBluetoothDevices = new ArrayList<>();
253            source.readParcelableList(supportedBluetoothDevices,
254                    ClassLoader.getSystemClassLoader());
255            return new CallAudioState(isMuted, route,
256                    supportedRouteMask, activeBluetoothDevice, supportedBluetoothDevices);
257        }
258
259        @Override
260        public CallAudioState[] newArray(int size) {
261            return new CallAudioState[size];
262        }
263    };
264
265    /**
266     * {@inheritDoc}
267     */
268    @Override
269    public int describeContents() {
270        return 0;
271    }
272
273    /**
274     * Writes AudioState object into a serializeable Parcel.
275     */
276    @Override
277    public void writeToParcel(Parcel destination, int flags) {
278        destination.writeByte((byte) (isMuted ? 1 : 0));
279        destination.writeInt(route);
280        destination.writeInt(supportedRouteMask);
281        destination.writeParcelable(activeBluetoothDevice, 0);
282        destination.writeParcelableList(new ArrayList<>(supportedBluetoothDevices), 0);
283    }
284
285    private static void listAppend(StringBuffer buffer, String str) {
286        if (buffer.length() > 0) {
287            buffer.append(", ");
288        }
289        buffer.append(str);
290    }
291}
292