1 /*
2  * Copyright 2021 HIMSA II K/S - www.himsa.com.
3  * Represented by EHIMA - www.ehima.com
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.bluetooth.le_audio;
19 
20 import android.bluetooth.BluetoothLeAudio;
21 import android.os.ParcelUuid;
22 import android.util.Log;
23 import android.util.Pair;
24 
25 import com.android.bluetooth.btservice.ServiceFactory;
26 
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.Map;
30 import java.util.Map.Entry;
31 import java.util.Objects;
32 import java.util.SortedSet;
33 import java.util.TreeSet;
34 
35 /** This class keeps Content Control Ids for LE Audio profiles. */
36 public class ContentControlIdKeeper {
37     private static final String TAG = "ContentControlIdKeeper";
38 
39     public static final int CCID_INVALID = 0;
40     public static final int CCID_MIN = 0x01;
41     public static final int CCID_MAX = 0xFF;
42 
43     private static SortedSet<Integer> sAssignedCcidList = new TreeSet();
44     private static HashMap<ParcelUuid, Pair<Integer, Integer>> sUuidToCcidContextPair =
45             new HashMap();
46     private static ServiceFactory sServiceFactory = null;
47 
initForTesting(ServiceFactory instance)48     static synchronized void initForTesting(ServiceFactory instance) {
49         sAssignedCcidList = new TreeSet();
50         sUuidToCcidContextPair = new HashMap();
51         sServiceFactory = instance;
52     }
53 
54     /**
55      * Functions is used to acquire Content Control ID (Ccid). Ccid is connected with a context type
56      * and the user uuid. In most of cases user uuid is the GATT service UUID which makes use of
57      * Ccid
58      *
59      * @param userUuid user identifier (GATT service)
60      * @param contextType the context types as defined in {@link BluetoothLeAudio}
61      * @return ccid to be used in the Gatt service Ccid characteristic.
62      */
acquireCcid(ParcelUuid userUuid, int contextType)63     public static synchronized int acquireCcid(ParcelUuid userUuid, int contextType) {
64         int ccid = CCID_INVALID;
65         if (contextType == BluetoothLeAudio.CONTEXT_TYPE_INVALID) {
66             Log.e(TAG, "Invalid context type value: " + contextType);
67             return ccid;
68         }
69 
70         // Remove any previous mapping
71         Pair<Integer, Integer> ccidContextPair = sUuidToCcidContextPair.get(userUuid);
72         if (ccidContextPair != null) {
73             releaseCcid(ccidContextPair.first);
74         }
75 
76         if (sAssignedCcidList.size() == 0) {
77             ccid = CCID_MIN;
78         } else if (sAssignedCcidList.last() < CCID_MAX) {
79             ccid = sAssignedCcidList.last() + 1;
80         } else if (sAssignedCcidList.first() > CCID_MIN) {
81             ccid = sAssignedCcidList.first() - 1;
82         } else {
83             int first_ccid_avail = sAssignedCcidList.first() + 1;
84             while (first_ccid_avail < CCID_MAX - 1) {
85                 if (!sAssignedCcidList.contains(first_ccid_avail)) {
86                     ccid = first_ccid_avail;
87                     break;
88                 }
89                 first_ccid_avail++;
90             }
91         }
92 
93         if (ccid != CCID_INVALID) {
94             sAssignedCcidList.add(ccid);
95             sUuidToCcidContextPair.put(userUuid, new Pair(ccid, contextType));
96 
97             if (sServiceFactory == null) {
98                 sServiceFactory = new ServiceFactory();
99             }
100             /* Notify LeAudioService about new ccid  */
101             LeAudioService service = sServiceFactory.getLeAudioService();
102             if (service != null) {
103                 service.setCcidInformation(userUuid, ccid, contextType);
104             }
105         }
106         return ccid;
107     }
108 
109     /**
110      * Release the acquired Ccid
111      *
112      * @param value Ccid value to release
113      */
releaseCcid(int value)114     public static synchronized void releaseCcid(int value) {
115         ParcelUuid uuid = null;
116 
117         for (Entry entry : sUuidToCcidContextPair.entrySet()) {
118             if (Objects.equals(value, ((Pair<Integer, Integer>) entry.getValue()).first)) {
119                 uuid = (ParcelUuid) entry.getKey();
120                 break;
121             }
122         }
123         if (uuid == null) {
124             Log.e(TAG, "Tried to remove an unknown CCID: " + value);
125             return;
126         }
127 
128         if (sAssignedCcidList.contains(value)) {
129             if (sServiceFactory == null) {
130                 sServiceFactory = new ServiceFactory();
131             }
132             /* Notify LeAudioService about new value  */
133             LeAudioService service = sServiceFactory.getLeAudioService();
134             if (service != null) {
135                 service.setCcidInformation(uuid, value, 0);
136             }
137 
138             sAssignedCcidList.remove(value);
139             sUuidToCcidContextPair.remove(uuid);
140         }
141     }
142 
143     /**
144      * Get Ccid information.
145      *
146      * @return Map of acquired ccids along with the user information.
147      */
148     public static synchronized Map<ParcelUuid, Pair<Integer, Integer>>
getUuidToCcidContextPairMap()149             getUuidToCcidContextPairMap() {
150         return Collections.unmodifiableMap(sUuidToCcidContextPair);
151     }
152 }
153