Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
QtAudioDeviceManager.java
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4package org.qtproject.qt.android.multimedia;
5
6import java.util.ArrayList;
7import java.util.Arrays;
8import java.util.Comparator;
9import java.util.HashMap;
10import java.util.List;
11import java.util.concurrent.CountDownLatch;
12import java.util.concurrent.atomic.AtomicInteger;
13import android.content.Context;
14import android.media.AudioDeviceCallback;
15import android.media.AudioDeviceInfo;
16import android.media.AudioFormat;
17import android.media.AudioManager;
18import android.media.AudioRecord;
19import android.media.AudioTrack;
20import android.media.MediaRecorder;
21import android.os.Build;
22import android.os.Handler;
23import android.os.HandlerThread;
24import android.util.Log;
25
26import org.qtproject.qt.android.UsedFromNativeCode;
27
28class QtAudioDeviceManager
29{
30 private static final String TAG = "QtAudioDeviceManager";
31
32 static private AudioManager m_audioManager = null;
33 static private final AudioDevicesReceiver m_audioDevicesReceiver = new AudioDevicesReceiver();
34 static private HandlerThread m_handlerThread = null;
35 static private Handler m_handler = null;
36 static private AudioRecord m_recorder = null;
37 static private AudioTrack m_streamPlayer = null;
38 static private Thread m_streamingThread = null;
39 static private boolean m_isStreaming = false;
40 static private boolean m_useSpeaker = false;
41 static private final int m_sampleRate = 8000;
42 static private final int m_channels = AudioFormat.CHANNEL_CONFIGURATION_MONO;
43 static private final int m_audioFormat = AudioFormat.ENCODING_PCM_16BIT;
44 static private final int m_bufferSize = AudioRecord.getMinBufferSize(m_sampleRate, m_channels, m_audioFormat);
45 static private int m_currentOutputId = -1;
46 static private AtomicInteger m_scoCounter = new AtomicInteger();
47 static private AtomicInteger m_communicationDeviceCounter = new AtomicInteger();
48 static private AtomicInteger m_speakerphoneCounter = new AtomicInteger();
49 static private int m_currentCommunicationDeviceId = -1;
50
51 // Holds the pointer to the QAndroidAudioDevices instance.
52 //
53 // This is always assigned with callbacks registered, from a
54 // C++ thread. Assignment only happens when QAndroidAudioDevices
55 // is constructed. It is always cleared from the Handler thread, using
56 // blocking job from the C++ thread.
57 static private long m_qAudioDevicesNativePtr = 0;
58
59 static native void onAudioInputDevicesUpdated(long qAudioDeviceNativePtr);
60 static native void onAudioOutputDevicesUpdated(long qAudioDeviceNativePtr);
61
62 static private void updateDeviceList() {
63 assert(m_handler.getLooper().isCurrentThread());
64 // Make sure we never get a callback when we are unregistered.
65 assert(m_qAudioDevicesNativePtr != 0);
66
67 if (m_currentOutputId != -1)
68 prepareAudioOutput(m_currentOutputId);
69 onAudioInputDevicesUpdated(m_qAudioDevicesNativePtr);
70 onAudioOutputDevicesUpdated(m_qAudioDevicesNativePtr);
71 }
72
73 private static class AudioDevicesReceiver extends AudioDeviceCallback {
75 public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
76 assert(m_handler.getLooper().isCurrentThread());
77 updateDeviceList();
78 }
79
81 public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
82 assert(m_handler.getLooper().isCurrentThread());
83 updateDeviceList();
84 }
85 }
86
87 @UsedFromNativeCode
88 static void qAndroidAudioDevicesConstructed(long nativePtr)
89 {
90 assert(nativePtr != 0);
91 assert(m_handlerThread == null);
92 m_qAudioDevicesNativePtr = nativePtr;
93 m_handlerThread = new HandlerThread("QtAudioDeviceCallbacks");
94 m_handlerThread.start();
95 m_handler = new Handler(m_handlerThread.getLooper());
96 m_audioManager.registerAudioDeviceCallback(m_audioDevicesReceiver, m_handler);
97 }
98
99 @UsedFromNativeCode
100 static void qAndroidAudioDevicesDestroyed()
101 {
102 assert(!m_handler.getLooper().isCurrentThread());
103
104 // Perform cleanup on the same thread that we receive callbacks, then wait for cleanup job
105 // to finish. No need for locks.
106 final CountDownLatch latch = new CountDownLatch(1);
107 final boolean postSuccess = m_handler.post(() -> {
108 m_audioManager.unregisterAudioDeviceCallback(m_audioDevicesReceiver);
109 assert(m_qAudioDevicesNativePtr != 0);
110 m_qAudioDevicesNativePtr = 0;
111 latch.countDown(); // Signal that the task is done
112 });
113
114 if (!postSuccess) {
115 Log.w(
116 TAG,
117 "Unable to post cleanup job to corresponding thread during " +
118 "QAndroidAudioDevices cleanup.");
119 return;
120 }
121
122 try {
123 latch.await();
124 } catch (Exception e) {
125 Log.w(
126 TAG,
127 "Unable to wait for cleanup job to finish on corresponding thread during" +
128 "QAndroidAudioDevices cleanup.");
129 }
130
131 m_handlerThread.quitSafely();
132 m_handlerThread = null;
133 m_handler = null;
134 }
135
136 static void setContext(Context context)
137 {
138 m_audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
139 }
140
141 static AudioDeviceInfo[] getAudioOutputDevices()
142 {
143 return getAudioDevices(AudioManager.GET_DEVICES_OUTPUTS);
144 }
145
146 static AudioDeviceInfo[] getAudioInputDevices()
147 {
148 return getAudioDevices(AudioManager.GET_DEVICES_INPUTS);
149 }
150
151 @UsedFromNativeCode
152 static AudioDeviceInfo getAudioInputDeviceInfo(int id)
153 {
154 final AudioDeviceInfo[] audioDevices = m_audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
155
156 for (AudioDeviceInfo deviceInfo : audioDevices) {
157 if (deviceInfo.getId() == id)
158 return deviceInfo;
159 }
160
161 return null;
162 }
163
164 @UsedFromNativeCode
165 static int getDefaultSampleRate()
166 {
167 String sampleRate = m_audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
168 return Integer.parseInt(sampleRate);
169 }
170
171 @UsedFromNativeCode
172 static boolean isBluetoothDevice(AudioDeviceInfo deviceInfo)
173 {
174 switch (deviceInfo.getType()) {
175 case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
176 case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
177 return true;
178 default:
179 return false;
180 }
181 }
182
183 @UsedFromNativeCode
184 static boolean prepareAudioInput(int id)
185 {
186 final AudioDeviceInfo[] audioDevices =
187 m_audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
188 for (AudioDeviceInfo deviceInfo : audioDevices) {
189 if (deviceInfo.getId() == id) {
190 switch (deviceInfo.getType())
191 {
192 case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
193 case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
194 case AudioDeviceInfo.TYPE_WIRED_HEADSET:
195 case AudioDeviceInfo.TYPE_USB_HEADSET:
196 case AudioDeviceInfo.TYPE_BUILTIN_MIC:
197 return prepareAudioDevice(deviceInfo, AudioManager.MODE_NORMAL);
198 default:
199 return true;
200 }
201 }
202 }
203 return false;
204 }
205
206 private static void setInputMuted(boolean mute)
207 {
208 // This method mutes the microphone across the entire platform
209 m_audioManager.setMicrophoneMute(mute);
210 }
211
212 private static boolean isMicrophoneMute()
213 {
214 return m_audioManager.isMicrophoneMute();
215 }
216
217 private static String audioDeviceTypeToString(int type)
218 {
219 // API <= 23 types
220 switch (type)
221 {
222 case AudioDeviceInfo.TYPE_AUX_LINE:
223 return "AUX Line";
224 case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
225 case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
226 return "Bluetooth";
227 case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
228 return "Built in earpiece";
229 case AudioDeviceInfo.TYPE_BUILTIN_MIC:
230 return "Built in microphone";
231 case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
232 return "Built in speaker";
233 case AudioDeviceInfo.TYPE_DOCK:
234 return "Dock";
235 case AudioDeviceInfo.TYPE_FM:
236 return "FM";
237 case AudioDeviceInfo.TYPE_FM_TUNER:
238 return "FM TUNER";
239 case AudioDeviceInfo.TYPE_HDMI:
240 return "HDMI";
241 case AudioDeviceInfo.TYPE_HDMI_ARC:
242 return "HDMI ARC";
243 case AudioDeviceInfo.TYPE_IP:
244 return "IP";
245 case AudioDeviceInfo.TYPE_LINE_ANALOG:
246 return "Line analog";
247 case AudioDeviceInfo.TYPE_LINE_DIGITAL:
248 return "Line digital";
249 case AudioDeviceInfo.TYPE_TV_TUNER:
250 return "TV tuner";
251 case AudioDeviceInfo.TYPE_USB_ACCESSORY:
252 return "USB accessory";
253 case AudioDeviceInfo.TYPE_USB_DEVICE:
254 return "USB device";
255 case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
256 return "Wired headphones";
257 case AudioDeviceInfo.TYPE_WIRED_HEADSET:
258 return "Wired headset";
259 }
260
261 // API 24
262 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
263 if (type == AudioDeviceInfo.TYPE_BUS)
264 return "Bus";
265 }
266
267 return "Unknown-Type";
268
269 }
270
271 private static boolean isDeviceTypeSupported(int type)
272 {
273 return audioDeviceTypeToString(type)
274 != audioDeviceTypeToString(AudioDeviceInfo.TYPE_UNKNOWN);
275 }
276
277 private static final HashMap<Integer, Integer> priorityMap = new HashMap<Integer, Integer>() {{
278 put(AudioDeviceInfo.TYPE_WIRED_HEADSET, 1);
279 put(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, 1);
280 put(AudioDeviceInfo.TYPE_USB_DEVICE, 1);
281 // NOTE: Should TYPE_USB_ACCESSORY also have priority level 1?
282 put(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, 2);
283 put(AudioDeviceInfo.TYPE_BLUETOOTH_SCO, 2);
284 put(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, 3);
285 }};
286 private static final int DEFAULT_PRIORITY = 4;
287
288 private static void sortAudioDevices(AudioDeviceInfo[] devices) {
289 Comparator<AudioDeviceInfo> deviceTypeComparator = new Comparator<AudioDeviceInfo>() {
290 @Override
291 public int compare(AudioDeviceInfo device1, AudioDeviceInfo device2) {
292 return getPriority(device1) - getPriority(device2);
293 }
294
295 private int getPriority(AudioDeviceInfo device) {
296 return priorityMap.getOrDefault(device.getType(), DEFAULT_PRIORITY);
297 }
298 };
299
300 Arrays.sort(devices, deviceTypeComparator);
301 }
302
303 private static AudioDeviceInfo[] getAudioDevices(int type)
304 {
305 ArrayList<AudioDeviceInfo> filteredDevices = new ArrayList<>();
306
307 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
308 boolean builtInMicAdded = false;
309 boolean bluetoothDeviceAdded = false;
310 AudioDeviceInfo[] audioDevices = m_audioManager.getDevices(type);
311 sortAudioDevices(audioDevices);
312
313 for (AudioDeviceInfo deviceInfo : audioDevices) {
314 if (!isDeviceTypeSupported(deviceInfo.getType()))
315 continue;
316
317 String deviceType = audioDeviceTypeToString(deviceInfo.getType());
318 if (deviceType.equals(audioDeviceTypeToString(AudioDeviceInfo.TYPE_BUILTIN_MIC))) {
319 if (builtInMicAdded) {
320 // Built in mic already added. Second built in mic is CAMCORDER, but there
321 // is no reliable way of selecting it. AudioSource.MIC usually means the
322 // primary mic. AudioSource.CAMCORDER source might mean the secondary mic,
323 // but there's no guarantee. It depends e.g. on the physical placement
324 // of the mics. That's why we will not add built in microphone twice.
325 // Should we?
326 continue;
327 }
328 builtInMicAdded = true;
329 } else if (isBluetoothDevice(deviceInfo)) {
330 if (bluetoothDeviceAdded) {
331 // Bluetooth device already added. Second device is just a different
332 // technology profille (like A2DP or SCO). We should not add the same
333 // device twice. Should we?
334 continue;
335 }
336 bluetoothDeviceAdded = true;
337 }
338
339 filteredDevices.add(deviceInfo);
340 }
341 }
342
343 return filteredDevices.toArray(new AudioDeviceInfo[filteredDevices.size()]);
344 }
345
346 final private static int [] bluetoothTypes = {
347 AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
348 AudioDeviceInfo.TYPE_BLUETOOTH_SCO
349 };
350 final private static int [] wiredTypes = {
351 AudioDeviceInfo.TYPE_WIRED_HEADSET,
352 AudioDeviceInfo.TYPE_WIRED_HEADPHONES
353 };
354
355 private static boolean containsAnyOfType(AudioDeviceInfo[] devices, int[]... types) {
356
357 return Arrays.stream(devices)
358 .anyMatch(device -> Arrays.stream(types)
359 .flatMapToInt(Arrays::stream)
360 .anyMatch(type -> device.getType() == type));
361 }
362
363 private static int getCorrectModeIfContainsAnyOfType(AudioDeviceInfo[] devices, int[]... types) {
364 return containsAnyOfType(devices, types) ?
365 AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_NORMAL;
366 }
367
368 private static int getModeForWired(AudioDeviceInfo[] audioDevices)
369 {
370 return getCorrectModeIfContainsAnyOfType(audioDevices, bluetoothTypes);
371 }
372
373 private static int getModeForBluetooth(AudioDeviceInfo[] audioDevices)
374 {
375 return getCorrectModeIfContainsAnyOfType(audioDevices, wiredTypes);
376 }
377
378 private static int getModeForSpeaker(AudioDeviceInfo[] audioDevices)
379 {
380 return getCorrectModeIfContainsAnyOfType(audioDevices, bluetoothTypes, wiredTypes);
381 }
382
383 private static boolean prepareAudioOutput(int id)
384 {
385 final AudioDeviceInfo[] audioDevices =
386 m_audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
387 for (AudioDeviceInfo deviceInfo : audioDevices) {
388 if (deviceInfo.getId() == id) {
389 switch (deviceInfo.getType())
390 {
391 case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
392 case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
393 return prepareAudioDevice(deviceInfo, getModeForBluetooth(audioDevices));
394 case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
395 return prepareAudioDevice(deviceInfo, getModeForSpeaker(audioDevices));
396 case AudioDeviceInfo.TYPE_WIRED_HEADSET:
397 case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
398 return prepareAudioDevice(deviceInfo, getModeForWired(audioDevices));
399 case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
400 // It doesn't work when WIRED HEADPHONES are connected
401 // Earpiece has the lowest priority and setWiredHeadsetOn(boolean)
402 // method to force it is deprecated
403 Log.w(TAG, "Built in Earpiece may not work when "
404 + "Wired Headphones are connected");
405 return prepareAudioDevice(deviceInfo, AudioManager.MODE_IN_CALL);
406 case AudioDeviceInfo.TYPE_HDMI:
407 case AudioDeviceInfo.TYPE_HDMI_ARC:
408 case AudioDeviceInfo.TYPE_HDMI_EARC:
409 return prepareAudioDevice(deviceInfo, AudioManager.MODE_NORMAL);
410 default:
411 return false;
412 }
413 }
414 }
415 return false;
416 }
417
422 private static AudioDeviceInfo getValidCommunicationDevice(AudioDeviceInfo device) {
423 if (device.isSink())
424 return device;
425
426 if (isBluetoothDevice(device)) {
427 // For Bluetooth sources, get output device with the same type and address
428 List<AudioDeviceInfo> communicationDevices = m_audioManager.getAvailableCommunicationDevices();
429 for (AudioDeviceInfo communicationDevice : communicationDevices) {
430 boolean isSameType = communicationDevice.getType() == device.getType();
431 boolean isSameAddress = communicationDevice.getAddress().equals(device.getAddress());
432 if (isSameType && isSameAddress)
433 return communicationDevice;
434 }
435
436 Log.w(TAG, "No matching bluetooth output device found for " + device);
437 }
438
439 return null;
440 }
441
442 private static boolean deviceIsCurrentCommunicationDevice(AudioDeviceInfo device) {
443 if (m_currentCommunicationDeviceId == -1 || device == null) {
444 return false;
445 }
446
447 return m_currentCommunicationDeviceId == device.getId();
448 }
449
450 @UsedFromNativeCode
451 static void releaseAudioDevice(int id) {
452 final AudioDeviceInfo[] devices = m_audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
454 if (device.getId() == id)
455 releaseAudioDevice(device);
456 }
457 }
458
462 private static void releaseAudioDevice(AudioDeviceInfo deviceInfo) {
463 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
464 // If device was used as the communication device, it should be cleared
465 AudioDeviceInfo communicationDevice = getValidCommunicationDevice(deviceInfo);
466 if (!deviceIsCurrentCommunicationDevice(communicationDevice))
467 return;
468
469 if (m_communicationDeviceCounter.decrementAndGet() == 0) {
470 m_audioManager.clearCommunicationDevice();
471 m_currentCommunicationDeviceId = -1;
472 }
473 } else if (isBluetoothDevice(deviceInfo) && m_audioManager.isBluetoothScoOn()
474 && m_scoCounter.decrementAndGet() == 0) {
475 m_audioManager.stopBluetoothSco();
476 m_audioManager.setBluetoothScoOn(false);
477 } else if (deviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
478 && m_speakerphoneCounter.decrementAndGet() == 0) {
479 m_audioManager.setSpeakerphoneOn(false);
480 }
481 }
482
483 private static boolean prepareAudioDevice(AudioDeviceInfo deviceInfo, int mode)
484 {
485 if (deviceInfo == null)
486 return false;
487
488 m_audioManager.setMode(mode);
489
490 boolean isSink = deviceInfo.isSink();
491 if (isSink)
492 m_currentOutputId = deviceInfo.getId();
493
494 boolean isBluetoothDevice = isBluetoothDevice(deviceInfo);
495 boolean isBluetoothSource = isBluetoothDevice && !isSink;
496 boolean isCommunicationMode = (mode == AudioManager.MODE_IN_CALL
497 || mode == AudioManager.MODE_IN_COMMUNICATION);
498
499 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
500 if (!isBluetoothSource && !isCommunicationMode)
501 return true;
502
503 // For communication modes and Bluetooth sources, it's required to set a communication device
504 AudioDeviceInfo communicationDevice = getValidCommunicationDevice(deviceInfo);
505 if (communicationDevice == null) {
506 Log.w(TAG, "No suitable communication device to set to enable communication via "
507 + deviceInfo.getId());
508 return false;
509 }
510
511 if (deviceIsCurrentCommunicationDevice(communicationDevice)) {
512 m_communicationDeviceCounter.incrementAndGet();
513 return true;
514 } else if (m_audioManager.setCommunicationDevice(communicationDevice)) {
515 // NOTE: Keep track of communication devices we set, as it takes time for it to be
516 // fully operational.
517 // TODO: Other applications can set a different communication device, in which case
518 // we should probably register a listener and clear our tracking when the
519 // communication device unexpectedly changes
520 m_currentCommunicationDeviceId = communicationDevice.getId();
521 m_communicationDeviceCounter.set(1);
522 return true;
523 }
524
525 return false;
526 }
527
528 boolean isSpeakerphoneDevice = deviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
529
530 if (isBluetoothSource && m_scoCounter.getAndIncrement() == 0) {
531 m_audioManager.startBluetoothSco();
532 m_audioManager.setBluetoothScoOn(true);
533 } else if (isSpeakerphoneDevice
534 && m_speakerphoneCounter.getAndIncrement() == 0) {
535 // TODO: Check if setting speakerphone is actually required for anything, it's not
536 // recommended in Android docs.
537 m_audioManager.setSpeakerphoneOn(true);
538 }
539
540 return true;
541 }
542
543 private static void streamSound()
544 {
545 byte data[] = new byte[m_bufferSize];
546 while (m_isStreaming) {
547 m_recorder.read(data, 0, m_bufferSize);
548 m_streamPlayer.play();
549 m_streamPlayer.write(data, 0, m_bufferSize);
550 m_streamPlayer.stop();
551 }
552 }
553
554 private static void startSoundStreaming(int inputId, int outputId)
555 {
556 if (m_isStreaming)
557 stopSoundStreaming();
558
559 m_recorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, m_sampleRate, m_channels,
560 m_audioFormat, m_bufferSize);
561 m_streamPlayer = new AudioTrack(AudioManager.STREAM_MUSIC, m_sampleRate, m_channels,
562 m_audioFormat, m_bufferSize, AudioTrack.MODE_STREAM);
563
564 final AudioDeviceInfo[] devices = m_audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
565 for (AudioDeviceInfo deviceInfo : devices) {
566 if (deviceInfo.getId() == outputId) {
567 m_streamPlayer.setPreferredDevice(deviceInfo);
568 } else if (deviceInfo.getId() == inputId) {
569 m_recorder.setPreferredDevice(deviceInfo);
570 }
571 }
572
573 m_recorder.startRecording();
574 m_isStreaming = true;
575
576 m_streamingThread = new Thread(new Runnable() {
577 @Override
578 public void run() {
579 streamSound();
580 }
581 });
582
583 m_streamingThread.start();
584 }
585
586 private static void stopSoundStreaming()
587 {
588 if (!m_isStreaming)
589 return;
590
591 m_isStreaming = false;
592 try {
593 m_streamingThread.join();
594 m_streamingThread = null;
595 } catch (InterruptedException e) {
596 e.printStackTrace();
597 }
598 m_recorder.stop();
599 m_recorder.release();
600 m_streamPlayer.release();
601 m_streamPlayer = null;
602 m_recorder = null;
603 }
604}
void AudioDeviceInfo()
[Audio callback capture setup peak meter]
Definition audio.cpp:278
IOBluetoothDevice * device
QPainter Context
static const QString context()
Definition java.cpp:396
QTCONCURRENT_RUN_NODISCARD auto run(QThreadPool *pool, Function &&f, Args &&...args)
#define assert
EGLDeviceEXT * devices
GLsizei GLenum GLenum * types
GLenum type
GLenum mode
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
[0]
@ Handler
static int compare(quint64 a, quint64 b)
static QInputDevice::DeviceType deviceType(const UINT cursorType)
manager put(request, myData, this, [this](QRestReply &reply) { if(reply.isSuccess()) })
[5]