1 /*
2  * KMix -- KDE's full featured mini mixer
3  *
4  * Copyright 2011 Igor Poboiko <igor.poboiko@gmail.com>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Library General Public
8  * License as published by the Free Software Foundation; either
9  * version 2 of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Library General Public License for more details.
15  *
16  * You should have received a copy of the GNU Library General Public
17  * License along with this program; if not, write to the Free
18  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  */
20 
21 #include "mixerengine.h"
22 #include "mixset_interface.h"
23 #include "mixer_interface.h"
24 #include "control_interface.h"
25 #include "mixerservice.h"
26 
27 #include <QIcon>
28 #include <QTimer>
29 
30 
31 const QString MixerEngine::KMIX_DBUS_SERVICE = "org.kde.kmix";
32 const QString MixerEngine::KMIX_DBUS_PATH = "/Mixers";
33 
MixerEngine(QObject * parent,const QVariantList & args)34 MixerEngine::MixerEngine(QObject *parent, const QVariantList &args)
35 	: Plasma::DataEngine(parent, args)
36 	, m_kmix(0)
37 {
38 	Q_UNUSED(args)
39 
40 	interface = QDBusConnection::sessionBus().interface();
41 	watcher = new QDBusServiceWatcher( this );
42 	watcher->addWatchedService( KMIX_DBUS_SERVICE );
43 	watcher->setConnection( QDBusConnection::sessionBus() );
44 	watcher->setWatchMode( QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration );
45 	connect( watcher, SIGNAL(serviceRegistered(QString)),
46 			this, SLOT(slotServiceRegistered(QString)) );
47 	connect( watcher, SIGNAL(serviceUnregistered(QString)),
48 			this, SLOT(slotServiceUnregistered(QString)) );
49 	init();
50 }
51 
~MixerEngine()52 MixerEngine::~MixerEngine()
53 {
54 	// Cleanup
55 	delete watcher;
56 	// it is bad idea to call removeSource() here
57 	clearInternalData(false);
58 	delete m_kmix;
59 }
60 
sources() const61 QStringList MixerEngine::sources() const
62 {
63 	QStringList sources;
64 	sources << "Mixers";
65 	return sources;
66 }
67 
init()68 void MixerEngine::init()
69 {
70 	getInternalData();
71 }
72 
createMixerInfo(const QString & dbusPath)73 MixerInfo* MixerEngine::createMixerInfo( const QString& dbusPath )
74 {
75 	MixerInfo* curmi = new MixerInfo;
76 	curmi->iface = new OrgKdeKMixMixerInterface( KMIX_DBUS_SERVICE, dbusPath,
77 					QDBusConnection::sessionBus(), this );
78 	curmi->id = curmi->iface->id();
79 	curmi->dbusPath = dbusPath;
80 	curmi->updateRequired = false;
81 	curmi->unused = false;
82 	curmi->connected = false;
83 	QDBusConnection::sessionBus().connect( KMIX_DBUS_SERVICE, dbusPath,
84 			"org.kde.KMix.Mixer", "changed",
85 			this, SLOT(slotControlsReconfigured()) );
86 	m_mixers.insert( dbusPath, curmi );
87 	return curmi;
88 }
89 
createControlInfo(const QString & mixerId,const QString & dbusPath)90 ControlInfo* MixerEngine::createControlInfo( const QString& mixerId, const QString& dbusPath )
91 {
92 	ControlInfo* curci = new ControlInfo;
93 	curci->iface = new OrgKdeKMixControlInterface( KMIX_DBUS_SERVICE, dbusPath,
94 						QDBusConnection::sessionBus(), this );
95 	curci->mixerId = mixerId;
96 	curci->id = curci->iface->id();
97 	curci->dbusPath = dbusPath;
98 	curci->updateRequired = false;
99 	curci->unused = false;
100 	m_controls.insert(mixerId, curci);
101 	return curci;
102 }
103 
getInternalData()104 void MixerEngine::getInternalData()
105 {
106 	clearInternalData(true);
107 	if ( !interface->isServiceRegistered( KMIX_DBUS_SERVICE ) )
108 		return;
109 	if ( !m_kmix )
110 	{
111 		m_kmix = new OrgKdeKMixMixSetInterface( KMIX_DBUS_SERVICE, KMIX_DBUS_PATH,
112 				QDBusConnection::sessionBus(), this );
113 		QDBusConnection::sessionBus().connect( KMIX_DBUS_SERVICE, KMIX_DBUS_PATH,
114 				"org.kde.KMix.MixSet", "mixersChanged",
115 				this, SLOT(slotMixersChanged()) );
116 		QDBusConnection::sessionBus().connect( KMIX_DBUS_SERVICE, KMIX_DBUS_PATH,
117 				"org.kde.KMix.MixSet", "masterChanged",
118 				this, SLOT(slotMasterChanged()) );
119 	}
120 
121 	for (const QString &path : m_kmix->mixers())
122 	{
123 		const MixerInfo *curmi = createMixerInfo( path );
124 		for (const QString &controlPath : curmi->iface->controls())
125 		{
126 			createControlInfo( curmi->id, controlPath );
127 		}
128 	}
129 	// Update "Mixers" source
130 	getMixersData();
131 }
132 
clearInternalData(bool removeSources)133 void MixerEngine::clearInternalData(bool removeSources)
134 {
135 	for (MixerInfo *mi : qAsConst(m_mixers))
136 	{
137 		if ( removeSources )
138 			removeSource( mi->id );
139 		delete mi->iface;
140 		delete mi;
141 	}
142 	m_mixers.clear();
143 
144 	for (ControlInfo *ci : qAsConst(m_controls))
145 	{
146 		if ( removeSources )
147 			removeSource( ci->mixerId + '/' + ci->id );
148 		delete ci->iface;
149 		delete ci;
150 	}
151 	m_controls.clear();
152 }
153 
sourceRequestEvent(const QString & name)154 bool MixerEngine::sourceRequestEvent( const QString &name )
155 {
156 	if ( name == "Mixers" )
157 		return getMixersData();
158 	else if ( name.indexOf("/") == -1 )
159 		// This request is for mixer
160 		return getMixerData( name );
161 	else
162 		// This request is for control
163 		return getControlData( name );
164 }
165 
updateSourceEvent(const QString & name)166 bool MixerEngine::updateSourceEvent( const QString &name )
167 {
168 	return sourceRequestEvent( name );
169 }
170 
getMixersData()171 bool MixerEngine::getMixersData()
172 {
173 	QStringList mixerIds;
174 	if ( interface->isServiceRegistered( KMIX_DBUS_SERVICE ) && m_kmix )
175 	{
176 		for (const MixerInfo *mi : qAsConst(m_mixers)) mixerIds.append(mi->id);
177 		/* FIXME: this is used to know whether kmix isn't running or
178 		 * it can't find any audio device; also it works as a strange
179 		 * workaround: without it there is no dataUpdated() call sometimes
180 		 * when it is updated here */
181 		setData( "Mixers", "Running", true );
182 		setData( "Mixers", "Mixers", mixerIds );
183 		setData( "Mixers", "Current Master Mixer", m_kmix->currentMasterMixer() );
184 		setData( "Mixers", "Current Master Control", m_kmix->currentMasterControl() );
185 	}
186 	else
187 	{
188 		setData( "Mixers", "Running", false );
189 		removeData( "Mixers", "Mixers" );
190 		removeData( "Mixers", "Current Master Mixer" );
191 		removeData( "Mixers", "Current Master Control" );
192 	}
193 	return true;
194 }
195 
getMixerData(const QString & source)196 bool MixerEngine::getMixerData( const QString& source )
197 {
198 	// Trying to find this mixer
199 	MixerInfo *curmi = 0;
200 	for (MixerInfo *mi : qAsConst(m_mixers))
201 	{
202 		if ( mi->id == source )
203 		{
204 			curmi = mi;
205 			break;
206 		}
207 	}
208 
209 	if ( !curmi || !curmi->iface->connection().isConnected() )
210 		return false;
211 	// Setting data
212 	curmi->updateRequired = true;
213 	QStringList controlIds;
214 	QStringList controlReadableNames;
215 	QStringList controlIcons;
216 	for (const ControlInfo *ci : m_controls.values(curmi->id))
217 	{
218 		if ( ci->iface->connection().isConnected() )
219 		{
220 			controlIds.append( ci->id );
221 			controlReadableNames.append( ci->iface->readableName() );
222 			controlIcons.append( ci->iface->iconName() );
223 		}
224 	}
225 
226 	setData( source, "Opened", curmi->iface->opened() );
227 	setData( source, "Readable Name", curmi->iface->readableName() );
228 	setData( source, "Balance", curmi->iface->balance() );
229 	setData( source, "Controls", controlIds );
230 	setData( source, "Controls Readable Names", controlReadableNames );
231 	setData( source, "Controls Icons Names", controlIcons );
232 	return true;
233 }
234 
getControlData(const QString & source)235 bool MixerEngine::getControlData( const QString &source )
236 {
237 	QString mixerId = source.section( '/', 0, 0 );
238 	QString controlId = source.section( '/', 1 );
239 	// Trying to find mixer for this control
240 	// and monitor for its changes
241 	for (MixerInfo *mi : qAsConst(m_mixers))
242 	{
243 		if ( mi->id == mixerId )
244 		{
245 			if ( !mi->connected )
246 			{
247 				QDBusConnection::sessionBus().connect( KMIX_DBUS_SERVICE, mi->dbusPath,
248 						"org.kde.KMix.Mixer", "controlChanged",
249 						this, SLOT(slotControlChanged()) );
250 				mi->connected = true;
251 			}
252 			break;
253 		}
254 	}
255 
256 	// Trying to find this control
257 	ControlInfo *curci = 0;
258 	for (ControlInfo *ci : m_controls.values(mixerId))
259 	{
260 		if ( ci->id == controlId ) {
261 			curci = ci;
262 			break;
263 		}
264 	}
265 
266 	if ( !curci || !curci->iface->connection().isConnected() )
267 		return false;
268 	// Setting data
269 	curci->updateRequired = true;
270 	setControlData( curci );
271 	return true;
272 }
273 
setControlData(ControlInfo * ci)274 void MixerEngine::setControlData(ControlInfo* ci)
275 {
276 	QString source = ci->mixerId + '/' + ci->id;
277 	setData( source, "Volume", ci->iface->volume() );
278 	setData( source, "Mute", ci->iface->mute() );
279 	setData( source, "Can Be Muted", ci->iface->canMute() );
280 	setData( source, "Readable Name", ci->iface->readableName() );
281 	setData( source, "Icon", QIcon::fromTheme(ci->iface->iconName()) );
282 	setData( source, "Record Source", ci->iface->recordSource() );
283 	setData( source, "Has Capture Switch", ci->iface->hasCaptureSwitch() );
284 }
285 
286 
slotServiceRegistered(const QString & serviceName)287 void MixerEngine::slotServiceRegistered( const QString &serviceName)
288 {
289 	// Let's give KMix some time to load
290 	if ( serviceName == KMIX_DBUS_SERVICE )
291 		QTimer::singleShot( 1000, this, SLOT(getInternalData()) );
292 }
293 
slotServiceUnregistered(const QString & serviceName)294 void MixerEngine::slotServiceUnregistered( const QString &serviceName)
295 {
296 	if ( serviceName == KMIX_DBUS_SERVICE )
297 		clearInternalData(true);
298 	// Updating 'Mixers' source
299 	getMixersData();
300 }
301 
slotControlChanged()302 void MixerEngine::slotControlChanged()
303 {
304 	// Trying to find mixer from which signal was emitted
305 	const MixerInfo *curmi = m_mixers.value( message().path(), 0 );
306 	if (curmi==nullptr) return;
307 	// Updating all controls that might change
308 	for (ControlInfo *ci : m_controls.values(curmi->id))
309 	{
310 		if (ci->updateRequired) setControlData(ci);
311 	}
312 }
313 
slotControlsReconfigured()314 void MixerEngine::slotControlsReconfigured()
315 {
316 	// Trying to find mixer from which signal was emitted
317 	const MixerInfo *curmi = m_mixers.value( message().path(), 0 );
318 	if (curmi==nullptr) return;
319 
320 	// Updating
321 	QList<ControlInfo*> controlsForMixer = m_controls.values( curmi->id );
322 	QStringList controlIds;
323 	QStringList controlReadableNames;
324 	QStringList controlIconNames;
325 	for (ControlInfo *ci : qAsConst(controlsForMixer)) ci->unused = true;
326 	for (const QString &controlPath : curmi->iface->controls())
327 	{
328 		ControlInfo* curci = 0;
329 		for (ControlInfo *ci : qAsConst(controlsForMixer))
330 		{
331 			if ( ci->dbusPath == controlPath )
332 			{
333 				curci = ci;
334 				break;
335 			}
336 		}
337 
338 		// If control not found then we should add a new
339 		if ( !curci )
340 			curci = createControlInfo( curmi->id, controlPath );
341 		curci->unused = false;
342 		controlIds.append( curci->id );
343 		controlReadableNames.append( curci->iface->readableName() );
344 		controlIconNames.append( curci->iface->iconName() );
345 	}
346 
347 	// If control is unused then we should remove it
348 	for (ControlInfo *ci : qAsConst(controlsForMixer))
349 	{
350 		if ( ci->unused )
351 		{
352 			m_controls.remove( curmi->id, ci );
353 			delete ci->iface;
354 			delete ci;
355 		}
356 	}
357 	if ( curmi->updateRequired )
358 	{
359 		setData( curmi->id, "Controls", controlIds );
360 		setData( curmi->id, "Controls Readable Names", controlReadableNames );
361 		setData( curmi->id, "Controls Icons Names", controlIconNames );
362 	}
363 }
364 
updateInternalMixersData()365 void MixerEngine::updateInternalMixersData()
366 {
367 	// Some mixer added or removed
368 	for (MixerInfo *mi : qAsConst(m_mixers)) mi->unused = true;
369 	for (const QString& mixerPath : m_kmix->mixers())
370 	{
371 		MixerInfo* curmi = m_mixers.value( mixerPath, 0 );
372 		// if mixer was added, we need to add one to m_mixers
373 		// and add all controls for this mixer to m_controls
374 		if ( !curmi )
375 		{
376 			curmi = createMixerInfo( mixerPath );
377 			for (const QString &controlPath : curmi->iface->controls())
378 				createControlInfo( curmi->id, controlPath );
379 		}
380 		curmi->unused = false;
381 	}
382 
383 	// and if it was removed, we should remove it
384 	// and remove all controls
385 	for (MixerInfo *mi : qAsConst(m_mixers))
386 	{
387 		if ( mi->unused )
388 		{
389 			for (ControlInfo *ci : m_controls.values(mi->id))
390 			{
391 				m_controls.remove( mi->id, ci );
392 				removeSource( ci->mixerId + '/' + ci->id );
393 				delete ci->iface;
394 				delete ci;
395 			}
396 			m_mixers.remove( mi->dbusPath );
397 			removeSource( mi->id );
398 			delete mi->iface;
399 			delete mi;
400 		}
401 	}
402 }
403 
slotMixersChanged()404 void MixerEngine::slotMixersChanged()
405 {
406 	// Let's give KMix some time to register this mixer on bus and so on
407 	QTimer::singleShot( 1000, this, SLOT(updateInternalMixersData()) );
408 }
409 
slotMasterChanged()410 void MixerEngine::slotMasterChanged()
411 {
412 	setData( "Mixers", "Current Master Mixer", m_kmix->currentMasterMixer() );
413 	setData( "Mixers", "Current Master Control", m_kmix->currentMasterControl() );
414 }
415 
serviceForSource(const QString & source)416 Plasma::Service* MixerEngine::serviceForSource(const QString& source)
417 {
418 	QString mixerId = source.section( '/', 0, 0 );
419 	QString controlId = source.section( '/', 1 );
420 	// Trying to find this control
421 	ControlInfo *curci = 0;
422 	for (ControlInfo *ci : m_controls.values(mixerId))
423 	{
424 		if ( ci->id == controlId ) {
425 			curci = ci;
426 			break;
427 		}
428 	}
429 
430 	if (curci==nullptr) return Plasma::DataEngine::serviceForSource(source);
431 	return (new MixerService(this, curci->iface));
432 }
433 
434 K_EXPORT_PLASMA_DATAENGINE_WITH_JSON(mixer, MixerEngine, "plasma-dataengine-mixer.json")
435 
436 #include "mixerengine.moc"
437