From: HyungKyu Song Date: Fri, 15 Feb 2013 15:12:14 +0000 (+0900) Subject: Tizen 2.0 Release X-Git-Tag: accepted/tizen_2.0/20130215.203908^0 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=edb6ca2f6f54bc64ae72ab408ef644eb2b2a63c3;p=framework%2Fconnectivity%2Fbluez.git Tizen 2.0 Release --- diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..2eaa329 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,61 @@ +Maxim Krasnyansky +Marcel Holtmann +Stephen Crane +Jean Tourrilhes +Jan Beutel +Ilguiz Latypov +Thomas Moser +Nils Faerber +Martin Leopold +Wolfgang Heidrich +Fabrizio Gennari +Brad Midgley +Henryk Ploetz +Philip Blundell +Johan Hedberg +Claudio Takahasi +Eduardo Rocha +Denis Kenzior +Frederic Dalleau +Frederic Danis +Luiz Augusto von Dentz +Fabien Chevalier +Ohad Ben-Cohen +Daniel Gollub +Tom Patzig +Kai Vehmanen +Vinicius Gomes +Alok Barsode +Bastien Nocera +Albert Huang +Glenn Durfee +David Woodhouse +Christian Hoene +Pekka Pessi +Siarhei Siamashka +Nick Pelly +Lennart Poettering +Gustavo Padovan +Marc-Andre Lureau +Bea Lam +Zygo Blaxell +Forrest Zhao +Scott Talbot +Ilya Rubtsov +Mario Limonciello +Filippo Giunchedi +Jaikumar Ganesh +Elvis Pfutzenreuter +Santiago Carot-Nemesio +José Antonio Santos Cadenas +Francisco Alecrim +Daniel Orstadius +Anderson Briglia +Anderson Lizardo +Bruna Moreira +Brian Gix +Andre Guedes +Sheldon Demario +Lucas De Marchi +Szymon Janc +Syam Sidhardhan diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..6d45519 --- /dev/null +++ b/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/COPYING.LIB b/COPYING.LIB new file mode 100644 index 0000000..1f7c8cc --- /dev/null +++ b/COPYING.LIB @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..0ea7db6 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,1722 @@ +ver 4.101: + Fix issue with missing BlueZ service file. + Fix issue with aborting A2DP setup during AVDTP start. + Fix issue with handling of multiple A2DP indication. + Fix issue with handling AVDTP abort with invalid SEID. + Fix issue with rejecting AVDTP abort commands. + Add support for handling AVDTP command collision. + +ver 4.100: + Fix issue with crashing when SCO connection fails. + Fix issue with HFP gateway failing on first GSM connection. + Fix issue with AVRCP and handling of vendor commands. + Fix issue with handling AVRCP subunit info command. + Fix issue with missing capability for AVRCP track reached end. + Fix issue with AVDTP signaling and GStreamer SBC NULL check. + Fix issue with AVDTP Reconfigure Reject message. + Fix issue with incorrect EIR length parsing. + Fix issue with SDP disconnect for HIDSDPDisable. + Fix issue with SDP interoperability with Mac OS X Lion. + Fix issue with reverse SDP discovery with some devices. + Fix issue with discovering state during power off operation. + Add support for AVRCP Volume Changed notifications. + Add support for AVRCP Set Absolute Volume handling. + Add support for display legacy PIN code agent method. + Add support for multiple media transports per endpoint. + Add support for discovering device information characteristics. + Add support for vendor source for Device ID setting. + Add support for immediate alert server. + Add support for link loss server. + + Notes: + This version requires D-Bus 1.4 or later. + This version requires GLib 2.28 or later. + +ver 4.99: + Fix issue with missing retries for BNEP connection setup. + Fix issue with not showing name if first EIR has no details. + Fix issue with running SDP discovery for LE devices. + Add support for GATT using 128-bit Bluetooth UUIDs. + Add support for retrieving key size information. + Add support for storing Long Term Keys. + Add support for Proximity Reporter API. + Add support for KeyboardDisplay IO capability. + Add support for version 1.0 of management API. + Add support for monitoring interface. + +ver 4.98: + Fix issue with adapter list upon initialization failure. + Fix issue with missing legacy property for Low Energy. + Fix issue with missing EIR information handling. + Fix issue with device address type tracking. + Fix issue with alert level characteristic. + Fix issue with headset shutdown handling. + Fix issue with Wiimote address handling. + Add support for advanced l2test options. + Add support for attribute protocol and multiple adapters. + +ver 4.97: + Update support for proximity profile. + Fix issue with SBC audio decoding quality. + Fix multiple issues with HFP support. + Fix multiple issues with A2DP support. + Fix multiple issues with AVDTP support. + Fix multiple issues with AVRCP support. + Add support for AVRCP meta-data transfer. + Add support for Bluetooth based thermometers. + +ver 4.96: + Fix issue with race condition in AVDTP stream start. + Fix issue with global adapter offline switching. + Fix issue with pairing and No Bonding devices. + Add support for Nintendo Wii Remote pairing. + +ver 4.95: + Fix issue with AVCTP replies with invalid PID. + Fix issue with AVRCP and unknown packet types. + Fix issue with AVRCP not using NOT_IMPLEMENTED correctly. + Fix issue with AVDTP discovery if all endpoints are in use. + Fix issue with invalid memory writes and media support. + Fix issue with not removing device alias and unbonding. + Fix issue with device disconnects and offline mode handling. + Add support for setting adapter name based on machine-info. + Add support for systemd service configuration. + +ver 4.94: + Fix issue with invalid read of memory in various modules. + Fix issue with buffer overflow when sending AVDTP commands. + Fix issue with response to vendor dependent AVRCP commands. + Fix issue with headset when not able to reply with ERROR. + Fix issue with crash when creating a device from storage. + Fix issue with handling non UTF-8 devices names. + Add support for improved discovery procedure. + +ver 4.93: + Fix issue with property type and Health Main channel. + Fix issue with crash when removing devices. + Add support for hid2hci and udev integration. + +ver 4.92: + Fix issue with handling of A2DP suspend response. + Fix issue with crashing when acquiring A2DP stream. + Fix issue with missing check for valid SCO before shutdown. + Fix issue with waiting for POLLERR when disconnecting SCO. + Fix issue with disconnect after primary service discovery. + Fix issue with attribute interface registration. + Add support for primary services over BR/EDR. + Add support for flushable packets of A2DP media. + +ver 4.91: + Fix issue with LMP version string and hciconfig. + Fix issue with missing discovery signal when scanning. + Fix issue with wrong state and canceling name resolving. + Fix issue with missing check during adapter initialization. + Fix issue with missing protocol not supported error and A2DP. + Fix issue with crash during driver unregistering and A2DP. + Fix issue with crash when receiving AVDTP close command. + Fix issue with remote SEP handling when A2DP codec changes. + Fix issue with SCO hangup handling and state changes. + Fix issue with security level and MCAP instances. + Fix issue with memory leak and HDP data channels. + Add support for discover characteristics by UUID to gatttool. + Add initial support for Out-of-Band association model. + Add initial support for SIM Access Profile. + +ver 4.90: + Fix issue with setting of global mode property. + Fix issue with handling of RequestSession responses. + Fix issue with TP_BNEP_CTRL_BV_01_C qualification test. + Fix issue with too short AVDTP request timeout. + Add support for SIM Access Profile manager. + Add support for new UUID utility functions. + Add support for attribute server notifications. + Add support for client characteristic configuration. + Update support for interactive GATT utility. + +ver 4.89: + Fix issue with name resolving when discovery is suspended. + Fix issue with parsing flags of advertising report. + Fix issue with SEP handling if interface is disabled. + Fix issue with device object creation on disconnect event. + Fix issue with indicators whenever the driver is initialized. + Fix issue with call indicator when parsing call info reply. + Fix issue with crash and allowed GATT MTU was too large. + Add support for SDP record of Primary GATT services. + Add support for interactive mode for GATT utility. + +ver 4.88: + Fix issue with HID channel reference count handling. + Fix issue with daemon exit on badly formatted AT+VTS. + Fix issue with crash while parsing of endpoint properties. + Fix issue with possible crash on AVDTP Suspend request timeout. + Fix issue with stopping inquiry before adapter is initialized. + Fix issue with creating device object when connection fails. + Fix issue with sending HCIDEVUP when adapter is already up. + Fix issue with handling bonding IO channel closing. + Fix agent cancellation in security mode 3 situations. + Update pairing code to support management interface. + +ver 4.87: + Fix issue with initialization when adapter is already up. + Fix issue with attribute server MTU and incoming connections. + Fix issue with duplicate characteristics after discovery. + +ver 4.86: + Revert wrong fix for SDP PDU size error response. + Fix various memory leaks in A2DP and AVDTP support. + Add Routing property to MediaTransport interface + Add proper tracking mechanism to NREC status. + Add READ_BLOB_REQUEST support to attribute server. + +ver 4.85: + Fix issue with event mask setting for older adapters. + Fix issue with device creation and pairing failures. + Add support for telephony support via oFono. + Add support for characteristic security level. + Update support for service registration. + +ver 4.84: + Fix issue with wrong parameters and device found signals. + Fix issue with leaking EIR data if RSSI does not change. + Fix issue with adapter initialization state. + Fix issue with closing of SDP server sockets. + +ver 4.83: + Fix issue with already connected HFP/HSP endpoints. + Fix missing reply when create device is canceled. + Fix memory leak within the attribute server. + Fix memory leak with unused extended inquiry name. + Fix setting paired state when device->authr is false. + Fix clearing authentication request for renewed keys. + Add support for storing link keys in runtime memory. + Update support for primary service discovery. + +ver 4.82: + Fix crash with mmap of files with multiples of page size. + Fix HFP response and hold (AT+BTRH) command response. + Fix device creation error response when powered off. + Fix device removal when connecting/browsing fails. + Add initial attribute permission implementation. + Add AVDTP SRC stream send buffer size verification. + Add support for setting link policy based on features. + +ver 4.81: + Fix issue with telephony driver initialization. + Fix issue with adapter services list initialization. + Fix crash after simultaneous authentication requests. + Add support for primary service search on device creation. + +ver 4.80: + Fix legacy link key storing for some buggy adapters. + Fix invalid memory access when EIR field length is zero. + Fix adapter initialization to wait for kernel HCI commands. + Fix initialization of adapters which are already up. + Fix possible race condition when initializing adapters. + Fix possible crashes when attempting to connect AVDTP. + Fix not aborting sink stream configuration on disconnect. + Fix not indicating disconnected state when connecting to AVDTP. + Fix not dropping AVDTP session when canceling stream setup. + Fix AVDTP abort not being send when the state is idle. + Fix regression with Low Energy and interleave discovery. + Add a new configuration option to disable Low Energy support. + Add iwmmxt optimization for SBC for ARM PXA series CPUs. + Update support for GATT Primary Service Discovery. + Update MCAP and HDP support. + +ver 4.79: + Fix issue with adapter initialization race condition. + Update new Bluetooth Management interface support. + +ver 4.78: + Fix various issues with AVDTP timer handling. + Fix various issues with handling of mode changes. + Fix issue with audio disconnect watch in connecting state. + Fix issue with handling call waiting indicators in telephony. + Fix issue with handling UUID parameter and RegisterEndpoint. + Add initial support for Bluetooth Management interface. + Add support for Application property to HealthChannel. + +ver 4.77: + Fix issue with device name and accessing already freed memory. + Fix issue with handling CHLD=0 command for handsfree. + Fix issue with manager properties and no adapters. + Fix issue with properties and broken service records. + Fix issue with A2DP playback and sample rate changes. + Update MCAP and HDP support. + +ver 4.76: + Fix issue in telephony driver with hanging up held call. + Fix issue in telephony driver with notifications when on hold. + Fix issue with blocking on setconf confirmation callback. + Fix issue with not always signaling new streams as sinks. + Fix issue with errors in case of endpoint request timeout. + Fix issue with HFP/HSP microphone and speaker gain values. + Add source if the device attempt to configure local sink stream. + Add PSM option for GATT/ATT over BR/EDR on gatttool. + Add support for GATT/ATT Attribute Write Request. + Update MCAP and HDP support. + +ver 4.75: + Fix use of uninitialized variable on legacy pairing. + Fix mismatch of attribute protocol opcode. + +ver 4.74: + Fix regression for Legacy Pairing. + Fix wrong PSM value for attribute protocol. + Fix issue with RSSI field in advertising reports. + Add support for Add BR/EDR and LE interleaved discovery. + Add support for GATT write characteristic value option. + Add support for specifying download address for AR300x. + +ver 4.73: + Fix problem with EIR data when setting the name. + Fix reading local name from command complete event. + Fix registering local endpoints with disabled socket interface. + Add support for more HCI operations using ops infrastructure. + Add support for GATT characteristic hierarchy. + Add support for GATT indications. + +ver 4.72: + Fix memory leak while connecting BTIO channels. + Fix crash with GStreamer plugin if SBC is not supported. + Fix issue with GATT server stop sending notifications. + Fix issue with GATT and dealing with the minimum MTU size. + Fix issue with file descriptor leak in GATT client. + Add support for UUID 128-bit handling in attribute client. + Add support for encoders/decoders for MTU Exchange. + Add support for the MTU Exchange procedure to the server. + Add support for a per channel MTU to the ATT server. + Add support for Characteristic interface. + Add support for new Media API and framework. + Add initial support for HDP plugin. + +ver 4.71: + Fix compilation when SBC support in not enabled. + Fix crash with RequestSession and application disconnects. + Fix memory leak and possible crash when removing audio device. + Fix issue with closing stream of locked sep when reconfiguring. + Fix issue where discovery could interfere with bonding. + Fix issue with Connected status when PS3 BD remote connects. + Fix issue with lifetime of fake input devices. + Add support for compile time option of oui.txt path. + Add support for printing IEEE1284 device ID for CUPS. + Add plugin for setting adapter class via DMI. + Add more features for attribute protocol and profile. + Add initial support for MCAP. + +ver 4.70: + Fix incoming call indication handling when in WAITING state. + Fix various SDP related qualification test case issues. + Fix logic to write EIR when SDP records are changed. + Fix UTF-8 validity check for remote names in EIR. + Add support for UUID-128 extended inquiry response. + Add service UUIDs from EIR to the DeviceFound signal. + Add fast connectable feature for Handsfree profile. + Add HCI command and event definitions for AMP support. + Add firmware download support for Qualcommh devices. + Add host level support for Atheros AR300x device. + Add initial support of ATT and GATT for basic rate. + +ver 4.69: + Fix issue with calling g_option_context_free() twice. + Fix inconsistencies with initial LE commands and events. + Add support for telephony ClearLastNumber method. + Add support for network server interface. + +ver 4.68: + Fix initialization of adapters in RAW mode. + Fix signal strength for HFP in Maemo's telephony support. + Add support for following the radio state via Maemo's MCE. + Add initial set of LE commands and events definitions. + Add mode option for L2CAP sockets to the BtIO API. + +ver 4.67: + Fix issue with authentication reply when bonding already completed. + Fix issue with not canceling authentication when bonding fails. + Fix issue with changed combination keys and temporary storage. + Fix issue with sdp_get_supp_feat library function. + Fix issue with missing unblock on device removal. + Fix issue with not waiting for mode change completion. + Add ARMv6 optimized version of analysis filter for SBC encoder. + +ver 4.66: + Fix regression with full debug enabling via SIGUSR2. + Fix redundant speaker/microphone gains being sent. + Fix not emitting PropertyChanged for SpeakerGain/MicrophoneGain. + Fix issue with storage usage when a record is not found in memory. + Fix issue with DiscoverServices not retrieving any records. + Fix audio profile disconnection order to match whitepaper. + Fix auto-accept confirmation when local agent has NoInputNoOutput. + Fix remote just-works SSP when MITM protection is required. + Fix performing dedicated bonding without MITM requirement. + Add support for storing debug link keys in runtime memory. + +ver 4.65: + Fix issues with general bonding being default setting now. + Fix driver removal upon device removal. + Add new "Blocked" property to device objects. + Add hciconfig support for blacklisting. + Add support for dynamic debug feature. + +ver 4.64: + Fix invalid memory access in headset_get_nrec function. + Fix issue with disconnect event on higher protocol layers. + Fix issue with list parsing in sdp_set_supp_features function. + Fix device object reference counting for SDP browse requests. + Add missing memory checks whenever memory is allocated for SDP. + Add support for exporting local services via D-Bus. + Add more L2CAP Enhanced Retransmission test options. + +ver 4.63: + Fix avdtp_abort not canceling pending requests. + Fix stale connection when abort gets rejected. + +ver 4.62: + Fix accidental symbol breakage with inquiry transmit power. + Fix using invalid data from previous headset connection. + Fix double free on AVDTP Abort response. + Fix possible crash while verifying AVDTP version. + Fix missing inuse flag when AVDTP stream is configured. + Add support for Bluetooth controller types. + +ver 4.61: + Fix issues with Read Inquiry Response Transmit Power Level. + Fix possible invalid read when removing a temporary device. + Fix mode restoration when remember_powered is false. + Fix conference call releasing in telephony-maemo. + Fix segmentation fault with authorization during headset disconnects. + Add support for handling unanswered AVDTP request on disconnect. + Add support for handling Inquiry Response Transmit Power Level. + Add support for caching of remote host features. + Add preliminary voice dialing support for HSP. + +ver 4.60: + Fix voice mailbox number reading from SIM. + Fix some races with D-Bus mainloop integration. + Add helpers for D-Bus signal watches. + +ver 4.59: + Add values for Bluetooth 4.0 specification. + Add SDP functions for HDP support. + Add test scripts for input and audio. + Fix missing close on BtIO create_io function. + Fix sending incorrect AVDTP commands after timeout occurs. + Fix timer removal when device disconnects unexpectedly. + Fix Extended Inquiry Response record for Device ID. + +ver 4.58: + Fix crash when adapter agent exists during authentication. + Fix CK-20W quirks for play and pause events. + +ver 4.57: + Fix unloading of drivers for uninitialized adapters. + Fix debug message to use requested and not opened SEID. + Fix codec selection for GStreamer plugin. + Fix deleting of SDP records during service updates. + Fix deleting of SDP records when a device is removed. + Fix handling when the SDP record is modified on remote device. + Fix potential buffer overflow by using snprintf instead of sprintf. + Fix const declarations for some storage function parameters. + +ver 4.56: + Add missing values from Bluetooth 3.0 specification. + Add proper tracking of device paired status. + Fix tracking of devices without permanently stored link key. + Fix issue with link key removal after connection failures. + Fix legacy pairing information based on remote host features. + Fix off-by-one issue with AVDTP capability parsing. + Fix AVRCP, AVCTP, AVDTP, A2DP and HFP version numbers. + Fix agent canceling before calling agent_destroy. + Fix service record parsing with an empty UUID list. + Fix various SDP related memory leaks. + +ver 4.55: + Add support for POSIX capabilities dropping. + Add special quirk for the Nokia CK-20W car kit. + Fix error code handling for AVDTP SetConfiguration response. + Fix updating out of range list when RSSI hasn't changed. + Fix various memory leaks and unnecessary error checks. + +ver 4.54: + Add introspection interface to output of introspection calls. + Fix stream handling when media transport disconnects prematurely. + Fix command timeout handling when there's no stream. + Fix headset_suspend_stream behavior for invalid states + Fix issue with AVDTP ABORTING state transition. + Fix issue with AVDTP suspend while closing. + +ver 4.53: + Fix issue with telephony connection state notifications. + Fix AVDTP stream leak for invalid media transport config. + Fix audio connection authorization handling with timeouts. + Fix race condition in authorizing audio connections. + Fix device authorized setting for AVRCP-only connections. + Fix duplicate attempts from device to connect signal channel. + +ver 4.52: + Add AVCTP support to test utility. + Fix AVDTP Abort when transport closes before response. + Fix authorization when the audio profiles are slow to connect. + Fix potential AVDTP reference leaks. + +ver 4.51: + Add utility for basic AVDTP testing. + Add support for configuring L2CAP FCS option. + Fix discovery mode for CUPS 1.4.x and later. + Fix global state tracking of audio service. + Fix last issues with the new build system. + +ver 4.50: + Fix issue with missing manual pages in distribution. + Fix issue with the configuration and state directories. + Fix issue with creating include directory. + Fix dependencies of include file generation. + +ver 4.49: + Add simple test program for basic GAP testing. + Add support for confirmation requests to agent example. + Add support for full non-recursive build. + Add five millisecond delay for Simple Pairing auto-accept. + Fix Class of Device setting when InitiallyPowered=false. + +ver 4.48: + Add library function for comparing UUID values. + Add support for creating all plugins as builtins. + Add support for async handling of service class changes. + Add support for source interface to audio IPC. + Fix device name settings when device is off or down. + Fix issue with enabled SCO server when not necessary. + Fix missing D-Bus access policy for CUPS backend. + Fix discovery results of CUPS backend. + Fix initialization handling of Maemo telephony. + +ver 4.47: + Add support for RFKILL unblock handling. + Add support for serial proxy configurations. + Add support for caching service class updates. + Fix issues with updating SDP service records. + Fix usage of limited discoverable mode. + Remove deprecated methods and signals for AudioSource. + +ver 4.46: + Add support for A2DP sink role. + Fix clearing svc_cache before the adapter is up. + Fix various pointer after free usages. + Fix various memory leaks. + +ver 4.45: + Fix UDEV_DATADIR fallback if pkg-config fails. + Fix adapter cleanup and setup prototypes. + Fix double-free with out-of-range devices. + Fix inband ring setting to be per-headset. + Fix handling of Maemo CSD startup. + +ver 4.44: + Add some missing manual pages. + Fix missing number prefix when installing udev rules. + Fix program prefix used in Bluetooth udev rules. + Fix three-way calling indicator order. + Fix downgrade/upgrade of callheld indicator. + Fix +CIEV sending when indicator value changes. + Fix signal handling for Maemo telephony driver. + Fix parsing issues with messages from Maemo CSD. + Fix issue with duplicate active calls. + +ver 4.43: + Add support for udev based on-demand startup. + Fix verbose error reporting of CUPS backend. + Fix various string length issues. + Fix issues with Maemo telephony driver. + Fix another device setup and temporary flag issue. + Fix and update example agent implementation. + +ver 4.42: + Add TI WL1271 to Texas Instruments chip list. + Add special udev mode to bluetoothd. + Fix regression when there is no agent registered. + Fix error return when bonding socket hang up. + Fix SCO server socket for HFP handsfree role. + Fix shutdown on SCO socket before closing. + Fix shutdown on A2DP audio stream channel before closing. + Fix issue with asserting on AVDTP reference count bugs. + Fix authorization denied issue with certain headsets. + Fix AVRCP UNITINFO and SUBUNIT INFO responses. + Fix discovery cancel issues in case SDP discovery fails. + +ver 4.41: + Fix pairing even if the ACL gets dropped before successful SDP. + Fix regression which caused device to be removed after pairing. + Fix HSP record fetching when remote device doesn't support it. + Fix SDP discovery canceling when clearing hs->pending. + Fix headset never connecting on the first attempt. + Fix headset state tracking if bt_search_service() fails. + Fix maximum headset connection count check. + Fix AVDTP Discover timeout handling. + Fix also UI_SET_KEYBIT for the new pause and play key codes. + +ver 4.40: + Add telephony driver for oFono telephony stack. + Add support for Dell specific HID proxy switching. + Add support for running hid2hci from udev. + Add mapping for AVRCP Play and Pause to dedicated key codes. + Fix AVRCP keycodes to better match existing X keymap support. + Fix various quoting issues within telephony support. + Fix memory allocation issue when generating PDUs for SDP. + Fix race condition on device removal. + Fix non-cancelable issue with CreateDevice method. + Fix non-working CancelDiscovery method call. + +ver 4.39: + Add workaround for dealing with unknown inquiry complete. + Fix discovering when using software scheduler. + Fix wrong NoInputNoOutput IO capability string. + Fix race condition with agent during pairing. + Fix agent cancellation for security mode 3 acceptor failure. + Fix temporary flag removal when device creation fails. + Fix hciattach to use ppoll instead of poll. + Fix service class update when adapter is down. + Fix service classes race condition during startup. + Fix release of audio client before freeing the device. + +ver 4.38: + Add support for builtin plugins. + Add framework for adapter operations. + Add constants for Enhanced Retransmission modes. + Fix HCI socket leak in device_remove_bonding. + Fix various format string issues. + Fix crashes with various free functions. + Fix issues with Headset and A2DP drivers to load again. + Fix sending AVRCP button released passthrough messages + Fix bug which prevent input devices to work after restart. + Fix issue with interpretation of UUID-128 as channel. + +ver 4.37: + Add version value for Bluetooth 3.0 devices. + Add additional L2CAP extended feature mask bits. + Add support for loading plugins in priority order. + Add support for more detailed usage of disconnect watches. + Add support for AVRCP volume control. + Add saturated clipping of SBC decoder output to 16-bit. + Fix potentially infinite recursion of adapter_up. + Fix SCO handling in the case of an incoming call. + Fix input service to use confirm callback. + Fix cleanup of temporary device entries from storage. + +ver 4.36: + Add proper tracking of AVCTP connect attempts. + Add support to channel pattern in Serial interface. + Fix A2DP sink crash if removing device while connecting. + Fix error handling if HFP indicators aren't initialized. + Fix segfault while handling an incoming SCO connection. + Fix Serial.Disconnect to abort connection attempt. + +ver 4.35: + Add support for Handsfree profile headset role. + Add additional checks for open SEIDs from clients. + Fix device removal while audio IPC client is connected. + Fix device removal when an authorization request is pending. + Fix incoming AVDTP connect while authorization in progress. + Fix disconnection timers for audio support. + Fix various potential NULL pointer deferences. + Fix callheld indicator value for multiple calls. + Fix voice number type usage. + Fix GDBus watch handling. + +ver 4.34: + Add support for version checks of plugins. + Add support for class property on adapter interface. + Add support for second SDP attempt after connection reset. + Add support for more detailed audio states. + Add support for HFP+A2DP auto connection feature. + Add support for new and improved audio IPC. + Add program for testing audio IPC interface. + Fix various AVDTP qualification related issues. + Fix broken SDP AttributeIdList parsing. + Fix invalid memory access of SDP URL handling. + Fix local class of device race conditions. + Fix issue with periodic inquiry on startup. + Fix missing temporary devices in some situations. + Fix SBC alignment issue for encoding with four subbands. + +ver 4.33: + Add Paired property to the DeviceFound signals. + Add support for Headset profile 1.2 version. + Fix broken network configuration when IPv6 is disabled. + Fix network regression that caused disconnection. + Fix SDP truncation of strings with NULL values. + Fix service discovery handling of CUPS helper. + +ver 4.32: + Fix broken SDP record handling. + Fix SDP data buffer parsing. + Fix more SDP memory leaks. + Fix read scan enable calls. + Fix A2DP stream handling. + +ver 4.31: + Add support for new BtIO helper library. + Fix AVDTP session close issue. + Fix SDP memory leaks. + Fix various uninitialized memory issues. + Fix duplicate signal emissions. + Fix property changes request handling. + Fix class of device storage handling. + +ver 4.30: + Add CID field to L2CAP socket address structure. + Fix reset of authentication requirements after bonding. + Fix storing of link keys when using dedicated bonding. + Fix storing of pre-Bluetooth 2.1 link keys. + Fix resetting trust settings on every reboot. + Fix handling of local name changes. + Fix memory leaks in hciconfig and hcitool + +ver 4.29: + Use AVRCP version 1.0 for now. + Decrease AVDTP idle timeout to one second. + Delay AVRCP connection when remote device connects A2DP. + Add workaround for AVDTP stream setup with broken headsets. + Add missing three-way calling feature bit for Handsfree. + Fix handsfree callheld indicator updating. + Fix parsing of all AT commands within the buffer. + Fix authentication replies when disconnected. + Fix handling of debug combination keys. + Fix handling of changed combination keys. + Fix handling of link keys when using no bonding. + Fix handling of invalid/unknown authentication requirements. + Fix closing of L2CAP raw socket used for dedicated bonding. + +ver 4.28: + Add AVDTP signal fragmentation support. + Add more SBC performance optimizations. + Add more SBC audio quality improvements. + Use native byte order for audio plugins. + Set the adapter alias only after checking the EIR data. + Fix auto-disconnect issue with explicit A2DP connections. + Fix invalid memory access of ALSA plugin. + Fix compilation with -Wsign-compare. + +ver 4.27: + Add more SBC optimization (MMX and ARM NEON). + Add BT_SECURITY and BT_DEFER_SETUP definitions. + Add support for deferred connection setup. + Add support for fragmentation of data packets. + Add option to trigger dedicated bonding. + Follow MITM requirements from remote device. + Require MITM for dedicated bonding if capabilities allow it. + Fix IO capabilities for non-pairing and pairing cases. + Fix no-bonding connections in non-bondable mode. + Fix new pairing detection with SSP. + Fix bonding with pre-2.1 devices and newer kernels. + Fix LIAC setting while toggling Pairable property. + Fix device creation for incoming security mode 3 connects. + Fix crash within A2DP with bogus pointer. + Fix issue with sdp_copy_record() function. + Fix crash with extract_des() if sdp_uuid_extract() fails. + +ver 4.26: + Use of constant shift in SBC quantization code. + Add possibility to analyze 4 blocks at once in encoder. + Fix correct handling of frame sizes in the encoder. + Fix for big endian problems in SBC codec. + Fix audio client socket to always be non-blocking. + Update telephony support for Maemo. + +ver 4.25: + Fix receiving data over the audio control socket. + Fix subbands selection for joint-stereo in SBC encoder. + Add new SBC analysis filter function. + +ver 4.24: + Fix signal emissions when removing adapters. + Fix missing adapter signals on exit. + Add support for bringing adapters down on exit. + Add support for RememberPowered option. + Add support for verbose compiler warnings. + Add more options to SBC encoder. + +ver 4.23: + Update audio IPC for better codec handling. + Fix bitstream optimization for SBC encoder. + Fix length header values of IPC messages. + Fix multiple coding style violations. + Fix FindDevice to handle temporary devices. + Add configuration option for DeviceID. + Add support for InitiallyPowered option. + Add missing signals for manager properties. + Add telephony support for Maemo. + +ver 4.22: + Add deny statements to D-Bus access policy. + Add support for LegacyPairing property. + Add support for global properties. + Add more commands to telephony testing script. + Add sender checks for serial and network interfaces. + Remove deprecated methods and signals from input interface. + Remove deprecated methods and signals from network interface. + Remove OffMode option and always use device down. + +ver 4.21: + Fix adapter initialization logic. + Fix adapter setup and start security manager early. + Fix usage issue with first_init variable. + +ver 4.20: + Cleanup session handling. + Cleanup mode setting handling. + Fix issue with concurrent audio clients. + Fix issue with HFP/HSP suspending. + Fix AT result code syntax handling. + Add Handsfree support for AT+NREC. + Add PairableTimeout adapter property. + +ver 4.19: + Fix installation of manual pages for old daemons. + Fix D-Bus signal emmissions for CreateDevice. + Fix issues with UUID probing. + Fix +BSRF syntax issue. + Add Pairable adapter property. + Add sdp_copy_record() library function. + +ver 4.18: + Fix release before close issue with RFCOMM TTYs. + Fix Connected property on input interface. + Fix DeviceFound signals during initial name resolving. + Fix service discovery handling. + Fix duplicate UUID detection. + Fix SBC gain mismatch and decoding handling. + Add more options to SBC encoder and decoder. + Add special any adapter object for service interface. + Add variable prefix to adapter and device object paths. + +ver 4.17: + Fix SBC encoder not writing last frame. + Fix missing timer for A2DP suspend. + Add more supported devices to hid2hci utility. + Add additional functionality to Handsfree support. + +ver 4.16: + Fix wrong parameter usage of watch callbacks. + Fix parameters for callback upon path removal. + Fix unloading of adapter drivers. + +ver 4.15: + Fix various A2DP state machine issues. + Fix some issues with the Handsfree error reporting. + Fix format string warnings with recent GCC versions. + Remove dependency on GModule. + +ver 4.14: + Fix types of property arrays. + Fix potential crash with input devices. + Fix PS3 BD remote input event generation. + Allow dynamic adapter driver registration. + Update udev rules. + +ver 4.13: + Fix service discovery and UUID handling. + Fix bonding issues with Simple Pairing. + Fix file descriptor misuse of SCO connections. + Fix various memory leaks in the device handling. + Fix AVCTP disconnect handling. + Fix GStreamer modes for MP3 encoding. + Add operator selection to Handsfree support. + +ver 4.12: + Fix crash with missing icon value. + Fix error checks of HAL plugin. + Fix SCO server socket cleanup on exit. + Fix memory leaks from DBusPendingCall. + Fix handling of pending authorization requests. + Fix missing protocol UUIDs in record pattern. + +ver 4.11: + Change SCO server socket into a generic one. + Add test script for dummy telephony plugin. + Fix uninitialized reply of multiple GetProperties methods. + +ver 4.10: + Fix memory leaks with HAL messages. + Add more advanced handsfree features. + Add properties to audio, input and network interfaces. + Stop device discovery timer on device removal. + +ver 4.9: + Fix signals for Powered and Discoverable properties. + Fix handling of Alias and Icon properties. + Fix duplicate entries for service UUIDs. + +ver 4.8: + Fix retrieving of formfactor value. + Fix retrieving of local and remote extended features. + Fix potential NULL pointer dereference during pairing. + Fix crash with browsing due to a remotely initated pairing. + +ver 4.7: + Fix pairing and service discovery logic. + Fix crashes during suspend and resume. + Fix race condition within devdown mode. + Add RequestSession and ReleaseSession methods. + Add Powered and Discoverable properties. + Add Devices property and deprecate ListDevices. + Add workaround for a broken carkit from Nokia. + +ver 4.6: + Fix Device ID record handling. + Fix service browsing and storage. + Fix authentication and encryption for input devices. + Fix adapter name initialization. + +ver 4.5: + Fix initialization issue with new adapters. + Send HID authentication request without blocking. + Hide the verbose SDP debug behind SDP_DEBUG. + Add extra UUIDs for service discovery. + Add SCO server socket listener. + Add authorization support to service plugin. + +ver 4.4: + Add temporary fix for the CUPS compile issue. + Add service-api.txt to distribution. + Mention the variable prefix of an object path + +ver 4.3: + Add dummy driver for telephony support. + Add support for discovery sessions. + Add service plugin for external services. + Various cleanups. + +ver 4.2: + Avoid memory copies in A2DP write routine. + Fix broken logic with Simple Pairing check and old kernels. + Allow non-bondable and outgoing SDP without agent. + Only remove the bonding for non-temporary devices. + Cleanup various unnecessary includes. + Make more unexported functions static. + Add basic infrastructure for gtk-doc support. + +ver 4.1: + Add 30 seconds timeout to BNEP connection setup phase. + Avoid memory copies in A2DP write routine for ALSA. + Make sure to include compat/sdp.h in the distribution. + +ver 4.0: + Initial public release. + +ver 3.36: + Add init routines for TI BRF chips. + Add extra attributes to the serial port record. + Add example record for headset audio gateway record. + Use Handsfree version 0x0105 for the gateway role. + Fix SDP record registration with specific record handles. + Fix BCSP sent/receive handling. + Fix various includes for cross-compilation. + Allow link mode settings for outgoing connections. + Allow bonding during periodic inquiry. + +ver 3.35: + Add two additional company identifiers. + Add UUID-128 support for service discovery. + Fix usage of friendly names for service discovery. + Fix authorization when experiemental is disabled. + Fix uninitialized variable in passkey request handling. + Enable output of timestamps for l2test and rctest. + +ver 3.34: + Replace various SDP functions with safe versions. + Add additional length validation for incoming SDP packets. + Use safe function versions for SDP client handling. + Fix issue with RemoveDevice during discovery procedure. + Fix collect for non-persistent service records. + +ver 3.33: + Add functions for reading and writing the link policy settings. + Add definition for authentication requirements. + Add support for handling Simple Pairing. + Add Simple Pairing support to Agent interface. + Add ReleaseMode method to Adapter interface. + Add DiscoverServices method to Device interface. + Remove obsolete code and cleanup the repository. + Move over to use the libgdbus API. + Enable PIE by default if supported. + +ver 3.32: + Add OCF constants for synchronous flow control enabling. + Add support for switching HID proxy devices from Dell. + Add more Bluetooth client/server helper functions. + Add support for input service idle timeout option. + Fix BNEP reconnection handling. + Fix return value for snd_pcm_hw_params() calls. + Use upper-case addresses for object paths. + Remove HAL support helpers. + Remove inotify support. + Remove service daemon activation handling. + Remove uneeded D-Bus API extension. + +ver 3.31: + Create device object for all pairing cases. + Convert authorization to internal function calls. + Add initial support for Headset Audio Gateway role. + Add generic Bluetooth helper functions for GLib. + Fix endiannes handling of connection handles. + Don't optimize when debug is enabled. + +ver 3.30: + Convert audio service into a plugin. + Convert input service into a plugin. + Convert serial service into a plugin. + Convert network service into a plugin. + Emit old device signals when a property is changed. + Fix missing DiscoverDevices and CancelDiscovery methods. + Add another company identifier. + Add basic support for Bluetooth sessions. + Add avinfo utility for AVDTP/A2DP classification. + Remove build option for deprecated sdpd binary. + +ver 3.29: + Introduce new D-Bus based API. + Add more SBC optimizations. + Add support for PS3 remote devices. + Fix alignment trap in SDP server. + Fix memory leak in sdp_get_uuidseq_attr function. + +ver 3.28: + Add support for MCAP UUIDs. + Add support for role switch for audio service. + Add disconnect timer for audio service. + Add disconnect detection to ALSA plugin. + Add more SBC optimizations. + Fix alignment issue of SDP server. + Remove support for SDP parsing via expat. + +ver 3.27: + Update uinput.h with extra key definitions. + Add support for input connect/disconnect callbacks. + Add ifdefs around some baud rate definitions. + Add another company identifier. + Add proper HFP service level connection handling. + Add basic headset automatic disconnect support. + Add support for new SBC API. + Fix SBC decoder noise at high bitpools. + Use 32-bit multipliers for further SBC optimization. + Check for RFCOMM connection state in SCO connect callback. + Make use of parameters selected in ALSA plugin. + +ver 3.26: + Fix compilation issues with UCHAR_MAX, USHRT_MAX and UINT_MAX. + Improve handling of different audio transports. + Enable services by default and keep old daemons disabled. + +ver 3.25: + Add limited support for Handsfree profile. + Add limited support for MPEG12/MP3 codec. + Add basic support for UNITINFO and SUBUNITINFO. + Add more SBC optimizations. + Fix external service (un)registration. + Allow GetInfo and GetAddress to fail. + +ver 3.24: + Add definitions for MDP. + Add TCP connection support for serial proxy. + Add fix for Logitech HID proxy switching. + Add missing macros, MIN, MAX, ABS and CLAMP. + Add more SBC encoder optimizations. + Add initial mechanism to handle headset commands. + Fix connecting to handsfree profile headsets. + Use proper function for checking signal name. + +ver 3.23: + Fix remote name request handling bug. + Fix key search function to honor the mmap area size. + Fix Avahi integration of network service. + Add new plugin communication for audio service. + Enable basic AVRCP support by default. + More optimizations to the SBC library. + Create common error definitions. + +ver 3.22: + Add missing include file from audio service. + Add SBC conformance test utility. + Add basic uinput support for AVRCP. + Fix L2CAP socket leak in audio service. + Fix buffer usage in GStreamer plugin. + Fix remote name request event handling. + +ver 3.21: + Add constant for Bluetooth socket options level. + Add initial AVRCP support. + Add A2DP sink support to GStreamer plugin. + Fix interoperability with A2DP suspend. + Fix sign error in 8-subband encoder. + Fix handling of service classes length size. + Store Extended Inquiry Response data information. + Publish device id information through EIR. + Support higher baud rates for Ericcson based chips. + +ver 3.20: + Fix GStreamer plugin file type detection. + Fix potential infinite loop in inotify support. + Fix D-Bus signatures for dict handling. + Fix issues with service activation. + Fix SDP failure handling of audio service. + Fix various memory leaks in input service. + Add secure device creation method to input service. + Add service information methods to serial service. + Add config file support to network service. + Add scripting capability to network service. + Add special on-mode handling. + Add optimization for SBC encoder. + Add tweaks for D-Bus 1.1.x libraries. + Add support for inquiry transmit power level. + +ver 3.19: + Limit range of bitpool announced while in ACP side. + Use poll instead of usleep to wait for worker thread. + Use default event mask from the specification. + Add L2CAP mode constants. + Add HID proxy support for Logitech diNovo Edge dongle. + Add refresh option to re-request device names. + Show correct connection link type. + +ver 3.18: + Don't allocate memory for the Bluetooth base UUID. + Implement proper locking for headsets. + Fix various A2DP SEP locking issues. + Fix and cleanup audio stream handling. + Fix stream starting if suspend request is pending. + Fix A2DP and AVDTP endianess problems. + Add network timeout and retransmission support. + Add more detailed decoding of EIR elements. + +ver 3.17: + Fix supported commands bit calculation. + Fix crashes in audio and network services. + Check PAN source and destination roles. + Only export the needed symbols for the plugins. + +ver 3.16: + Update company identifier list. + Add support for headsets with SCO audio over HCI. + Add support for auto-create through ALSA plugin. + Add support for ALSA plugin parameters. + Add GStreamer plugin with SBC decoder and encoder. + Fix network service NAP, GN and PANU servers. + Set EIR information from SDP database. + +ver 3.15: + Add A2DP support to the audio service. + Add proxy support to the serial service. + Extract main service class for later use. + Set service classes value from SDP database. + +ver 3.14: + Add missing signals for the adapter interface. + Add definitions and functions for Simple Pairing. + Add basic commands for Simple Pairing. + Add correct Simple Pairing and EIR interaction. + Add missing properties for remote information. + Add EPoX endian quirk to the input service. + Fix HID descriptor import and storage functions. + Fix handling of adapters in raw mode. + Fix remote device listing methods. + +ver 3.13: + Fix some issues with the headset support. + Fix concurrent pending connection attempts. + Fix usage of devname instead of netdev. + Add identifier for Nokia SyncML records. + Add command for reading the CSR chip revision. + Add generic CSR radio test support. + Update HCI command table. + +ver 3.12: + Add missing HCI command text descriptions + Add missing HCI commands structures. + Add missing HCI event structures. + Add common bachk() function. + Add support for limited discovery mode. + Add support for setting of event mask. + Add GetRemoteServiceIdentifiers method. + Add skeleton for local D-Bus server. + Add headset gain control methods. + Fix various headset implementation issues. + Fix various serial port service issues. + Fix various input service issues. + Let CUPS plugin discover printers in range. + Improve the BCM2035 UART init routine. + Ignore connection events for non-ACL links. + +ver 3.11: + Update API documentation. + Minimize SDP root records and browse groups. + Use same decoder for text and URL strings. + Fix URL data size handling. + Fix SDP pattern extraction for XML. + Fix network connection persistent state. + Add network connection helper methods. + Add initial version of serial port support. + Add class of device tracking. + +ver 3.10.1: + Add option to disable installation of manual pages. + Fix input service encryption setup. + Fix serial service methods. + Fix network service connection handling. + Provide a simple init script. + +ver 3.10: + Add initial version of network service. + Add initial version of serial service. + Add initial version of input service. + Add initial version of audio service. + Add authorization framework. + Add integer based SBC library. + Add version code for Bluetooth 2.1 specification. + Add ESCO_LINK connection type constant. + Export sdp_uuid32_to_uuid128() function. + +ver 3.9: + Add RemoteDeviceDisconnectRequested signal. + Add updated service framework. + Add embedded GLib library. + Add support for using system GLib library. + Create internal SDP server library. + +ver 3.8: + Sort discovered devices list based on their RSSI. + Send DiscoverableTimeoutChanged signal. + Fix local and remote name validity checking. + Add ListRemoteDevices and ListRecentRemoteDevices methods. + Add basic integration of confirmation concept. + Add support for service record description via XML. + Add support for external commands to the RFCOMM utility. + Add experimental service and authorization API. + Add functions for registering binary records. + +ver 3.7: + Fix class of device handling. + Fix error replies with pairing and security mode 3. + Fix disconnect method for RFCOMM connections. + Add match pattern for service searches. + Add support for prioritized watches. + Add additional PDU length checks. + Fix CSRC value for partial responses. + +ver 3.6.1: + Fix IO channel race conditions. + Fix pairing issues on big endian systems. + Fix pairing issues with page timeout errors. + Fix pairing state for security mode 3 requests. + Switch to user as default security manager mode. + +ver 3.6: + Update D-Bus based RFCOMM interface support. + Use L2CAP raw sockets for HCI connection creation. + Add periodic discovery support to the D-Bus interface. + Add initial support for device names via EIR. + Add proper UTF-8 validation of device names. + Add support for the J-Three keyboard. + Fix issues with the asynchronous API for SDP. + +ver 3.5: + Fix and cleanup watch functionality. + Add support for periodic inquiry mode. + Add support for asynchronous SDP requests. + Add more request owner tracking. + Add asynchronous API for SDP. + Document pageto and discovto options. + +ver 3.4: + Improve error reporting for failed HCI commands. + Improve handling of CancelBonding. + Fixed bonding reply message when disconnected. + Fix UUID128 string lookup handling. + Fix malloc() versus bt_malloc() usage. + +ver 3.3: + Don't change inquiry mode for Bluetooth 1.1 adapters. + Add udev rules for Bluetooth serial PCMCIA cards. + Add Cancel and Release methods for passkey agents. + Add GetRemoteClass method. + Convert to using ppoll() and pselect(). + Initialize allocated memory to zero. + Remove bcm203x firmware loader. + Remove kernel specific timeouts. + Add additional private data field for SDP sessions. + Add host controller to host flow control defines. + Add host number of completed packets defines. + Initialize various memory to zero before usage. + +ver 3.2: + Only check for the low-level D-Bus library. + Update possible device minor classes. + Fix timeout for pending reply. + Add more Inquiry with RSSI quirks. + Sleep only 100 msecs for device detection. + Don't send BondingCreated on link key renewal. + Allow storing of all UTF-8 remote device names. + Create storage filenames with a generic function. + Fix handling of SDP strings. + Add adapter type for SDIO cards. + Add features bit for link supervision timeout. + +ver 3.1: + Add missing placeholders for feature bits. + Fix handling of raw mode devices. + Fix busy loop in UUID extraction routine. + Remove inquiry mode setting. + Remove auth and encrypt settings. + +ver 3.0: + Implement the new BlueZ D-Bus API. + Fix broken behavior with EVT_CMD_STATUS. + Add features bit for pause encryption. + Add additional EIR error code. + Add more company identifiers. + Add another Phonebook Access identifier. + Update sniff subrating data structures. + +ver 2.25: + Use %jx instead of %llx for uint64_t and int64_t. + Allow null-terminated text strings. + Add UUID for N-Gage games. + Add UUID for Apple Macintosh Attributes. + Add Apple attributes and iSync records. + Add definitions for Apple Agent. + Add support for the Handsfree Audio Gateway service. + Add support for choosing a specific record handle. + Add support for dialup/telephone connections. + Add definitions for Apple Agent. + Add support for record handle on service registration. + +ver 2.24: + Fix display of SDP text and data strings. + Add support for device scan property. + Add support for additional access protocols. + Update the D-Bus policy configuration file. + +ver 2.23: + Update the new D-Bus interface. + Make dfutool ready for big endian architectures. + Add support for AVRCP specific service records. + Add support for writing complex BCCMD commands. + Add the new BCCMD interface utility. + Add MicroBCSP implementation from CSR. + Add constants and definitions for sniff subrating. + Add support for allocation of binary text elements. + Add HCI emulation tool. + Add fake HID support for old EPoX presenters. + Reject connections from unknown HID devices. + Fix service discovery deadlocks with Samsung D600 phones. + +ver 2.22: + Remove D-Bus 0.23 support. + Add initial version of the new D-Bus interface. + Add support for extended inquiry response commands. + Add support for the Logitech diNovo Media Desktop Laser. + Add compile time buffer checks (FORTIFY SOURCE). + Decode reserved LMP feature bits. + Fix errno overwrite problems. + Fix profile descriptor problem with Samsung phones. + +ver 2.21: + Move create_dirs() and create_file() into the textfile library. + Let textfile_put() also replace the last key value pair. + Fix memory leaks with textfile_get() usage. + Fix infinite loops and false positive matches. + Don't retrieve stored link keys for RAW devices. + Document the putkey and delkey commands. + Show supported commands also in clear text. + Support volatile changes of the BD_ADDR for CSR chips. + Add support for identification of supported commands. + Add missing OCF declarations for the security filter. + Add two new company identifiers. + +ver 2.20: + Add UUIDs for video distribution profile. + Add UUIDs for phonebook access profile. + Add attribute identifier for supported repositories. + Add definitions for extended inquiry response. + Add functions for extended inquiry response. + Add support for extended inquiry response. + Add support for HotSync service record. + Add support for ActiveSync service record. + Add ActiveSync networking support. + Fix D-Bus crashes with new API versions. + +ver 2.19: + Fix the GCC 4.0 warnings. + Fix the routing for dealing with raw devices. + Fix off by one memory allocation error. + Fix security problem with escape characters in device name. + Add per device service record functions. + Send D-Bus signals for inquiry results and remote name resolves. + Add support for device specific SDP records. + +ver 2.18: + Support D-Bus 0.23 and 0.33 API versions. + Support reading of complex BCCMD values. + Support minimum and maximum encryption key length. + Add support for reading and writing the inquiry scan type. + Add definitions for connection accept timeout and scan enable. + Add support for inquiry scan type. + Add tool for the CSR BCCMD interface. + Add first draft of the Audio/Video control utility. + Add disconnect timer support for the A2DP ALSA plugin. + Make SBC parameters configurable. + Replace non-printable characters in device names. + Remove hci_vhci.h header file. + Remove hci_uart.h header file. + +ver 2.17: + Set the storage directory through ${localstatedir}. + Add the textfile library for ASCII based file access. + Add support for return link keys event. + Add support for voice setting configuration. + Add support for page scan timeout configuration. + Add support for storing and deleting of stored link keys. + Add support for searching for services with UUID-128. + Add support for retrieving all possible service records. + Add support for a raw mode view of service records. + Add support for HID information caching in hidd. + Add support for authentication in pand and dund. + Add support for changing BD_ADDR of CSR chips. + Add pskey utility for changing CSR persistent storage values. + Add the firmware upgrade utility. + Add connection caching for the A2DP ALSA plugin. + Add functions for stored link keys. + Add definitions for PIN type and unit key. + Add SDP_WAIT_ON_CLOSE flag for sdp_connect(). + Include stdio.h in bluetooth.h header file. + Include sys/socket.h in the header files. + +ver 2.16: + Store link keys in ASCII based file format. + Support device name caching. + Support zero length data sizes in l2test. + Change default l2ping data size to 44 bytes. + Hide the server record and the public browse group root. + Read BD_ADDR if not set and if it is a raw device. + Add SDP language attributes. + Add support for browsing the L2CAP group. + Add support for stored pin codes for outgoing connections. + Add support for local commands and extended features. + Add support for reading CSR panic and fault codes. + Add config option for setting the inquiry mode. + Add OUI decoding support. + Use unlimited inquiry responses as default. + Use cached device names for PIN request. + Use the clock offset when getting the remote names. + Add function for reading local supported commands. + Add function for reading local extended features. + Add function for reading remote extended features. + Add function for getting the remote name with a clock offset. + Add function for extracting the OUI from a BD_ADDR. + Add inquiry info structure with RSSI and page scan mode. + Fix buffer allocation for features to string conversion. + Support inquiry with unlimited number of responses. + +ver 2.15: + Enable the RFCOMM service level security. + Add deprecated functions for reading the name. + Add command for reading the clock offset. + Add command for reading the clock. + Add function for reading the clock. + Add function for reading the local Bluetooth address. + Add function for reading the local supported features. + Don't configure raw devices. + Don't set inquiry scan or page scan on raw devices. + Don't show extended information for raw devices. + Support L2CAP signal sizes bigger than 2048 bytes. + Cleanup of the socket handling code of the test programs. + Use better way for unaligned access. + Remove sdp_internal.h and its usage. + +ver 2.14: + Make use of additional connection information. + Use library function for reading the RSSI. + Use library function for reading the link quality. + Use library function for reading the transmit power level. + Use library functions for the link supervision timeout. + Add tool for changing the device address. + Add function for reading the RSSI. + Add function for reading the link quality. + Add function for reading the transmit power level. + Add functions for the link supervision timeout. + Remove deprecated functions. + Update AM_PATH_BLUEZ macro. + +ver 2.13: + Use file permission 0600 for the link key file. + Add support for HID attribute descriptions. + Add support for Device ID attributes. + Add Device ID and HID attribute definitions. + Update the UUID constants and its translations. + Update L2CAP socket option definitions. + Update connection information definitions. + Various whitespace cleanups. + +ver 2.12: + Inherit the device specific options from the default. + Use --device for selecting the source device. + Add --nosdp option for devices with resource limitation. + Add support and parameter option for secure mode. + Add a lot of build ids and hardware revisions. + Add service classes and profile ids for WAP. + Add simple AM_PATH_BLUEZ macro. + Update UUID translation tables. + Correct kernel interface for CMTP and HIDP support. + +ver 2.11: + Initial support for the kernel security manager. + Various cleanups to avoid inclusion of kernel headers. + Fix output when the CUPS backend is called without arguments. + Fix problems with a 64 bit userland. + Use Bluetooth library functions if available. + Use standard numbering scheme of SDP record handles. + Use bit zero for vendor packets in the filter type bitmask. + Add SIM Access types for service discovery. + Add more audio/video profile translations. + Add another company identifier. + Add the missing HCI error codes. + Add RFCOMM socket options. + Add definition for the SECURE link mode. + Add functions for reading and writing the inquiry mode. + Add functions for AFH related settings and information. + Add version identifier for the Bluetooth 2.0 specification. + Add a master option to the hidd. + Add support for changing the link key of a connection. + Add support for requesting encryption on keyboards. + Add support for revision information of Digianswer devices. + Add support for the Zoom, IBM and TDK PCMCIA cards. + Add checks for the OpenOBEX and the ALSA libraries. + Add experimental mRouter support. + +ver 2.10: + Use a define for the configuration directory. + Fix string initialization for flags translation. + Fix and extend the unaligned access macros. + Make compiling with debug information optional. + Don't override CFLAGS from configure. + Check for usb_get_busses() and usb_interrupt_read(). + Add optional support for compiling with PIE. + Make installation of the init scripts optional. + Make compiling with debug information optional. + Don't override CFLAGS from configure. + +ver 2.9: + Retry SDP connect if busy in the CUPS backend. + Use packet type and allow role switch in hcitool. + Use the functions from the USB library for hid2hci. + Add Broadcom firmware loader. + Add EPoX endian quirk for buggy keyboards. + Add L2CAP info type and info result definitions. + Add value for L2CAP_CONF_RFC_MODE. + Change RSSI value to signed instead of unsigned. + Allow UUID32 values as protocol identifiers. + Update the autoconf/automake scripts. + +ver 2.8: + Use LIBS and LDADD instead of LDFLAGS. + Use HIDP subclass field for HID boot protocol. + Set olen before calling getsockopt() in pand. + Restore signals for dev-up script. + Add PID file support for pand. + Add size parameter to expand_name() in hcid. + Add support for audio source and audio sink SDP records. + Add support for HID virtual cable unplug. + Add support for AmbiCom BT2000C card. + Add defines and UUID's for audio/video profiles. + Add AVDTP protocol identifier. + Add HIDP subclass field. + Add PKGConfig support. + Fix the event code of inquiry with RSSI. + Remove dummy SDP library. + +ver 2.7: + Fix display of decoded LMP features. + Update company identifiers. + Add AFH related types. + Add first bits from EDR prototyping specification. + Add support for inquiry with RSSI. + Add HCRP related SDP functions. + Add HIDP header file. + Add support for getting the AFH channel map. + Add support for AFH mode. + Add support for inquiry mode. + Add Bluetooth backend for CUPS. + Add the hid2hci utility. + Add the hidd utility. + Add the pand utility. + Add the dund utility. + More endian bug fixes. + Give udev some time to create the RFCOMM device nodes. + Release the TTY if no device node is found. + New startup script for the Bluetooth subsystem. + Update to the autoconf stuff. + +ver 2.6: + Change default prefix to /usr. + Add manpages for hcid and hcid.conf. + Add the sdpd server daemon. + Add the sdptool utility. + Add the ciptool utility. + Add new company identifiers. + Add BNEP and CMTP header files. + Add the SDP library. + Use R2 for default value of pscan_rep_mode. + +ver 2.5: + Add decoding of Bluetooth 1.2 features. + Add link manager version parameter for Bluetooth 1.2. + Add new company identifiers. + Add D-Bus support for PIN request. + Support for transmit power level. + Support for park, sniff and hold mode. + Support for role switch. + Support for reading the clock offset. + Support for requesting authentication. + Support for setting connection encryption. + Show revision information for Broadcom devices. + Replace unprintable characters in device name. + Use R1 for default value of pscan_rep_mode. + Fix some 64-bit problems. + Fix some endian problems. + Report an error on PIN helper failure. + Update bluepin script for GTK2. + +ver 2.4: + Increase number of inquiry responses. + Support for transmit power level. + Display all 8 bytes of the features. + Add support for reading and writing of IAC. + Correct decoding class of device. + Use Ericsson revision command for ST Microelectronics devices. + Display AVM firmware version with 'revision' command. + New code for CSR specific revision information. + Support for ST Microelectronics specific initialization. + Support for 3Com card version 3.0. + Support for TDK, IBM and Socket cards. + Support for initial baud rate. + Update man pages. + Fixes for some memory leaks. + +ver 2.3: + Added const qualifiers to appropriate function arguments. + Minor fixes. + CSR firmware version is now displayed by 'revision' command. + Voice command is working properly on big endian machines. + Added support for Texas Bluetooth modules. + Added support for high UART baud rates on Ericsson modules. + BCSP initialization fixes. + Support for role switch command (hcitool). + RFCOMM config file parser fixes. + Update man pages. + Removed GLib dependency. + +ver 2.2: + Updated RFCOMM header file. + Additional HCI command and event defines. + Support for voice settings (hciconfig). + Minor hcitool fixes. + Improved configure script. + Added Headset testing tool. + Updated man pages. + RPM package. + +ver 2.1.1: + Resurrect hci_remote_name. + +ver 2.1: + Added hci_{read, write}_class_of_dev(). + Added hci_{read, write}_current_iac_lap(). + Added hci_write_local_name(). + Added RFCOMM header file. + Minor fixes. + Improved BCSP initialization (hciattach). + Support for displaying link quality (hcitool). + Support for changing link supervision timeout (hcitool). + New RFCOMM TTY configuration tool (rfcomm). + Minor fixes and updates. + +ver 2.0: + Additional company IDs. + BCSP initialization (hciattach). + Minor hciconfig fixes. + +ver 2.0-pr13: + Support for multiple pairing modes. + Link key database handling fixes. + +ver 2.0-pre12: + Removed max link key limit. Keys never expire. + Link key database is always updated. Reread PIN on SIGHUP (hcid). + Bluetooth script starts SDPd, if installed. + Other minor fixes. + +ver 2.0-pre11: + Improved link key management and more verbose logging (hcid). + Fixed scan command (hcitool). + +ver 2.0-pre10: + Fix hci_inquiry function to return errors and accept user buffers. + New functions hci_devba, hci_devid, hci_for_each_dev and hci_get_route. + Additional company IDs. + Makefile and other minor fixes. + Support for reading RSSI, remote name and changing + connection type (hcitool). + Device initialization fixes (hcid). + Other minor fixes and improvements. + Build environment cleanup and fixes. + +ver 2.0-pre9: + Improved bluepin. Working X authentication. + Improved hcitool. New flexible cmd syntax, additional commands. + Human readable display of the device features. + LMP features to string translation support. + Additional HCI command and event defines. + Extended hci_filter API. + +ver 2.0-pre8: + Additional HCI ioctls and defines. + All strings and buffers are allocated dynamically. + ba2str, str2ba automatically swap bdaddress. + Additional hciconfig commands. Support for ACL and SCO MTU ioctls. + Support for Inventel and COM1 UART based devices. + Minor hcitool fixes. + Improved l2test. New L2CAP test modes. + Minor fixes and cleanup. + +ver 2.0-pre7: + Bluetooth libraries and header files is now a separate package. + New build environment uses automake and libtool. + Massive header files cleanup. + Bluetooth utilities is now a separate package. + New build environment uses automake. + Moved all config files and security data to /etc/bluetooth. + Various cleanups. + +ver 2.0-pre6: + API cleanup and additions. + Improved hcitool. + l2test minor output fixes. + hciattach opt to display list of supported devices. + +ver 2.0-pre4: + HCI filter enhancements. + +ver 2.0-pre3: + Cleanup. + +ver 2.0-pre2: + Additional HCI library functions. + Improved CSR baud rate initialization. + PCMCIA scripts fixes and enhancements. + Documentation update. + +ver 2.0-pre1: + New UART initialization utility. + Hot plugging support for UART based PCMCIA devices. + SCO testing utility. + New authentication utility (bluepin). + Minor fixes and improvements. diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..56b077d --- /dev/null +++ b/INSTALL @@ -0,0 +1,236 @@ +Installation Instructions +************************* + +Copyright (C) 1994, 1995, 1996, 1999, 2000, 2001, 2002, 2004, 2005 Free +Software Foundation, Inc. + +This file is free documentation; the Free Software Foundation gives +unlimited permission to copy, distribute and modify it. + +Basic Installation +================== + +These are generic installation instructions. + + The `configure' shell script attempts to guess correct values for +various system-dependent variables used during compilation. It uses +those values to create a `Makefile' in each directory of the package. +It may also create one or more `.h' files containing system-dependent +definitions. Finally, it creates a shell script `config.status' that +you can run in the future to recreate the current configuration, and a +file `config.log' containing compiler output (useful mainly for +debugging `configure'). + + It can also use an optional file (typically called `config.cache' +and enabled with `--cache-file=config.cache' or simply `-C') that saves +the results of its tests to speed up reconfiguring. (Caching is +disabled by default to prevent problems with accidental use of stale +cache files.) + + If you need to do unusual things to compile the package, please try +to figure out how `configure' could check whether to do them, and mail +diffs or instructions to the address given in the `README' so they can +be considered for the next release. If you are using the cache, and at +some point `config.cache' contains results you don't want to keep, you +may remove or edit it. + + The file `configure.ac' (or `configure.in') is used to create +`configure' by a program called `autoconf'. You only need +`configure.ac' if you want to change it or regenerate `configure' using +a newer version of `autoconf'. + +The simplest way to compile this package is: + + 1. `cd' to the directory containing the package's source code and type + `./configure' to configure the package for your system. If you're + using `csh' on an old version of System V, you might need to type + `sh ./configure' instead to prevent `csh' from trying to execute + `configure' itself. + + Running `configure' takes awhile. While running, it prints some + messages telling which features it is checking for. + + 2. Type `make' to compile the package. + + 3. Optionally, type `make check' to run any self-tests that come with + the package. + + 4. Type `make install' to install the programs and any data files and + documentation. + + 5. You can remove the program binaries and object files from the + source code directory by typing `make clean'. To also remove the + files that `configure' created (so you can compile the package for + a different kind of computer), type `make distclean'. There is + also a `make maintainer-clean' target, but that is intended mainly + for the package's developers. If you use it, you may have to get + all sorts of other programs in order to regenerate files that came + with the distribution. + +Compilers and Options +===================== + +Some systems require unusual options for compilation or linking that the +`configure' script does not know about. Run `./configure --help' for +details on some of the pertinent environment variables. + + You can give `configure' initial values for configuration parameters +by setting variables in the command line or in the environment. Here +is an example: + + ./configure CC=c89 CFLAGS=-O2 LIBS=-lposix + + *Note Defining Variables::, for more details. + +Compiling For Multiple Architectures +==================================== + +You can compile the package for more than one kind of computer at the +same time, by placing the object files for each architecture in their +own directory. To do this, you must use a version of `make' that +supports the `VPATH' variable, such as GNU `make'. `cd' to the +directory where you want the object files and executables to go and run +the `configure' script. `configure' automatically checks for the +source code in the directory that `configure' is in and in `..'. + + If you have to use a `make' that does not support the `VPATH' +variable, you have to compile the package for one architecture at a +time in the source code directory. After you have installed the +package for one architecture, use `make distclean' before reconfiguring +for another architecture. + +Installation Names +================== + +By default, `make install' will install the package's files in +`/usr/local/bin', `/usr/local/man', etc. You can specify an +installation prefix other than `/usr/local' by giving `configure' the +option `--prefix=PREFIX'. + + You can specify separate installation prefixes for +architecture-specific files and architecture-independent files. If you +give `configure' the option `--exec-prefix=PREFIX', the package will +use PREFIX as the prefix for installing programs and libraries. +Documentation and other data files will still use the regular prefix. + + In addition, if you use an unusual directory layout you can give +options like `--bindir=DIR' to specify different values for particular +kinds of files. Run `configure --help' for a list of the directories +you can set and what kinds of files go in them. + + If the package supports it, you can cause programs to be installed +with an extra prefix or suffix on their names by giving `configure' the +option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'. + +Optional Features +================= + +Some packages pay attention to `--enable-FEATURE' options to +`configure', where FEATURE indicates an optional part of the package. +They may also pay attention to `--with-PACKAGE' options, where PACKAGE +is something like `gnu-as' or `x' (for the X Window System). The +`README' should mention any `--enable-' and `--with-' options that the +package recognizes. + + For packages that use the X Window System, `configure' can usually +find the X include and library files automatically, but if it doesn't, +you can use the `configure' options `--x-includes=DIR' and +`--x-libraries=DIR' to specify their locations. + +Specifying the System Type +========================== + +There may be some features `configure' cannot figure out automatically, +but needs to determine by the type of machine the package will run on. +Usually, assuming the package is built to be run on the _same_ +architectures, `configure' can figure that out, but if it prints a +message saying it cannot guess the machine type, give it the +`--build=TYPE' option. TYPE can either be a short name for the system +type, such as `sun4', or a canonical name which has the form: + + CPU-COMPANY-SYSTEM + +where SYSTEM can have one of these forms: + + OS KERNEL-OS + + See the file `config.sub' for the possible values of each field. If +`config.sub' isn't included in this package, then this package doesn't +need to know the machine type. + + If you are _building_ compiler tools for cross-compiling, you should +use the `--target=TYPE' option to select the type of system they will +produce code for. + + If you want to _use_ a cross compiler, that generates code for a +platform different from the build platform, you should specify the +"host" platform (i.e., that on which the generated programs will +eventually be run) with `--host=TYPE'. + +Sharing Defaults +================ + +If you want to set default values for `configure' scripts to share, you +can create a site shell script called `config.site' that gives default +values for variables like `CC', `cache_file', and `prefix'. +`configure' looks for `PREFIX/share/config.site' if it exists, then +`PREFIX/etc/config.site' if it exists. Or, you can set the +`CONFIG_SITE' environment variable to the location of the site script. +A warning: not all `configure' scripts look for a site script. + +Defining Variables +================== + +Variables not defined in a site shell script can be set in the +environment passed to `configure'. However, some packages may run +configure again during the build, and the customized values of these +variables may be lost. In order to avoid this problem, you should set +them in the `configure' command line, using `VAR=value'. For example: + + ./configure CC=/usr/local2/bin/gcc + +causes the specified `gcc' to be used as the C compiler (unless it is +overridden in the site shell script). Here is a another example: + + /bin/bash ./configure CONFIG_SHELL=/bin/bash + +Here the `CONFIG_SHELL=/bin/bash' operand causes subsequent +configuration-related scripts to be executed by `/bin/bash'. + +`configure' Invocation +====================== + +`configure' recognizes the following options to control how it operates. + +`--help' +`-h' + Print a summary of the options to `configure', and exit. + +`--version' +`-V' + Print the version of Autoconf used to generate the `configure' + script, and exit. + +`--cache-file=FILE' + Enable the cache: use and save the results of the tests in FILE, + traditionally `config.cache'. FILE defaults to `/dev/null' to + disable caching. + +`--config-cache' +`-C' + Alias for `--cache-file=config.cache'. + +`--quiet' +`--silent' +`-q' + Do not print messages saying which checks are being made. To + suppress all normal output, redirect it to `/dev/null' (any error + messages will still be shown). + +`--srcdir=DIR' + Look for the package's source code in directory DIR. Usually + `configure' can determine that directory automatically. + +`configure' also accepts some other, not widely useful, options. Run +`configure --help' for more details. + diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..ecbe335 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,490 @@ + +AM_MAKEFLAGS = --no-print-directory + +lib_LTLIBRARIES = + +noinst_LIBRARIES = + +noinst_LTLIBRARIES = + +bin_PROGRAMS = + +sbin_PROGRAMS = + +noinst_PROGRAMS = + +dist_man_MANS = + +dist_noinst_MANS = + +CLEANFILES = + +EXTRA_DIST = + +includedir = @includedir@/bluetooth + +include_HEADERS = + +AM_CFLAGS = $(WARNING_CFLAGS) $(MISC_CFLAGS) +AM_LDFLAGS = $(MISC_LDFLAGS) + +if DATAFILES +dbusdir = $(sysconfdir)/dbus-1/system.d +dbusservicedir = $(datadir)/dbus-1/system-services + +dbus_DATA = src/bluetooth.conf +dbusservice_DATA = src/org.bluez.service + +confdir = $(sysconfdir)/bluetooth + +conf_DATA = + +statedir = $(localstatedir)/lib/bluetooth + +state_DATA = + +if SYSTEMD +systemdunitdir = @SYSTEMD_UNITDIR@ + +systemdunit_DATA = src/bluetooth.service +endif +endif + +plugindir = $(libdir)/bluetooth/plugins + +if MAINTAINER_MODE +build_plugindir = $(abs_top_srcdir)/plugins/.libs +else +build_plugindir = $(plugindir) +endif + + +plugin_LTLIBRARIES = + + +lib_headers = lib/bluetooth.h lib/hci.h lib/hci_lib.h lib/mgmt.h \ + lib/sco.h lib/l2cap.h lib/sdp.h lib/sdp_lib.h lib/uuid.h \ + lib/rfcomm.h lib/bnep.h lib/cmtp.h lib/hidp.h lib/a2mp.h +local_headers = $(foreach file,$(lib_headers), lib/bluetooth/$(notdir $(file))) + +BUILT_SOURCES = $(local_headers) src/builtin.h + +include_HEADERS += $(lib_headers) + +lib_LTLIBRARIES += lib/libbluetooth.la + +lib_libbluetooth_la_SOURCES = $(lib_headers) \ + lib/bluetooth.c lib/hci.c lib/sdp.c lib/uuid.c +lib_libbluetooth_la_LDFLAGS = $(AM_LDFLAGS) -version-info 16:0:13 +lib_libbluetooth_la_DEPENDENCIES = $(local_headers) + +noinst_LTLIBRARIES += lib/libbluetooth-private.la + +lib_libbluetooth_private_la_SOURCES = $(lib_libbluetooth_la_SOURCES) + +if SBC +noinst_LTLIBRARIES += sbc/libsbc.la + +sbc_libsbc_la_SOURCES = sbc/sbc.h sbc/sbc.c sbc/sbc_math.h sbc/sbc_tables.h \ + sbc/sbc_primitives.h sbc/sbc_primitives.c \ + sbc/sbc_primitives_mmx.h sbc/sbc_primitives_mmx.c \ + sbc/sbc_primitives_iwmmxt.h sbc/sbc_primitives_iwmmxt.c \ + sbc/sbc_primitives_neon.h sbc/sbc_primitives_neon.c \ + sbc/sbc_primitives_armv6.h sbc/sbc_primitives_armv6.c + +sbc_libsbc_la_CFLAGS = $(AM_CFLAGS) -finline-functions -fgcse-after-reload \ + -funswitch-loops -funroll-loops + +noinst_PROGRAMS += sbc/sbcinfo sbc/sbcdec sbc/sbcenc + +sbc_sbcdec_SOURCES = sbc/sbcdec.c sbc/formats.h +sbc_sbcdec_LDADD = sbc/libsbc.la + +sbc_sbcenc_SOURCES = sbc/sbcenc.c sbc/formats.h +sbc_sbcenc_LDADD = sbc/libsbc.la + +if SNDFILE +noinst_PROGRAMS += sbc/sbctester + +sbc_sbctester_LDADD = @SNDFILE_LIBS@ -lm +sbc_sbctest_CFLAGS = $(AM_CFLAGS) @SNDFILE_CFLAGS@ +endif +endif + +attrib_sources = attrib/att.h attrib/att-database.h attrib/att.c \ + attrib/gatt.h attrib/gatt.c \ + attrib/gattrib.h attrib/gattrib.c attrib/client.h \ + attrib/client.c attrib/gatt-service.h attrib/gatt-service.c + +gdbus_sources = gdbus/gdbus.h gdbus/mainloop.c gdbus/watch.c \ + gdbus/object.c gdbus/polkit.c + +btio_sources = btio/btio.h btio/btio.c + +builtin_modules = +builtin_sources = +builtin_nodist = +mcap_sources = + +if MCAP +mcap_sources += health/mcap_lib.h health/mcap_internal.h \ + health/mcap.h health/mcap.c \ + health/mcap_sync.c +endif + +if PNATPLUGIN +builtin_modules += pnat +builtin_sources += plugins/pnat.c +endif + +if AUDIOPLUGIN +builtin_modules += audio +builtin_sources += audio/main.c \ + audio/manager.h audio/manager.c \ + audio/gateway.h audio/gateway.c \ + audio/headset.h audio/headset.c \ + audio/control.h audio/control.c \ + audio/avctp.h audio/avctp.c \ + audio/avrcp.h audio/avrcp.c \ + audio/device.h audio/device.c \ + audio/source.h audio/source.c \ + audio/sink.h audio/sink.c \ + audio/a2dp.h audio/a2dp.c \ + audio/avdtp.h audio/avdtp.c \ + audio/ipc.h audio/ipc.c \ + audio/unix.h audio/unix.c \ + audio/media.h audio/media.c \ + audio/transport.h audio/transport.c \ + audio/telephony.h audio/a2dp-codecs.h +builtin_nodist += audio/telephony.c + +noinst_LIBRARIES += audio/libtelephony.a + +audio_libtelephony_a_SOURCES = audio/telephony.h audio/telephony-dummy.c \ + audio/telephony-maemo5.c audio/telephony-ofono.c \ + audio/telephony-maemo6.c audio/telephony-tizen.c +endif + +if SAPPLUGIN +builtin_modules += sap +builtin_sources += sap/main.c \ + sap/manager.h sap/manager.c \ + sap/server.h sap/server.c \ + sap/sap.h + +builtin_nodist += sap/sap.c + +noinst_LIBRARIES += sap/libsap.a + +sap_libsap_a_SOURCES = sap/sap.h sap/sap-dummy.c sap/sap-u8500.c +endif + +if INPUTPLUGIN +builtin_modules += input +builtin_sources += input/main.c \ + input/manager.h input/manager.c \ + input/server.h input/server.c \ + input/device.h input/device.c \ + input/fakehid.c input/fakehid.h +endif + +if SERIALPLUGIN +builtin_modules += serial +builtin_sources += serial/main.c \ + serial/manager.h serial/manager.c \ + serial/proxy.h serial/proxy.c \ + serial/port.h serial/port.c +endif + +if NETWORKPLUGIN +builtin_modules += network +builtin_sources += network/main.c \ + network/manager.h network/manager.c \ + network/common.h network/common.c \ + network/server.h network/server.c \ + network/connection.h network/connection.c +endif + +if SERVICEPLUGIN +builtin_modules += service +builtin_sources += plugins/service.c +endif + +if HEALTHPLUGIN +builtin_modules += health +builtin_sources += health/hdp_main.c health/hdp_types.h \ + health/hdp_manager.h health/hdp_manager.c \ + health/hdp.h health/hdp.c \ + health/hdp_util.h health/hdp_util.c +endif + +if GATTMODULES +builtin_modules += thermometer alert time gatt_example proximity \ + deviceinfo +builtin_sources += thermometer/main.c \ + thermometer/manager.h thermometer/manager.c \ + thermometer/thermometer.h thermometer/thermometer.c \ + alert/main.c alert/server.h alert/server.c \ + time/main.c time/server.h time/server.c \ + plugins/gatt-example.c \ + proximity/main.c proximity/manager.h proximity/manager.c \ + proximity/monitor.h proximity/monitor.c \ + proximity/reporter.h proximity/reporter.c \ + proximity/linkloss.h proximity/linkloss.c \ + proximity/immalert.h proximity/immalert.c \ + deviceinfo/main.c \ + deviceinfo/manager.h deviceinfo/manager.c \ + deviceinfo/deviceinfo.h deviceinfo/deviceinfo.c +endif + + +builtin_modules += hciops mgmtops +builtin_sources += plugins/hciops.c plugins/mgmtops.c + +if HAL +builtin_modules += hal +builtin_sources += plugins/hal.c +else +builtin_modules += formfactor +builtin_sources += plugins/formfactor.c +endif + +EXTRA_DIST += plugins/hal.c plugins/formfactor.c + +builtin_modules += storage +builtin_sources += plugins/storage.c + +builtin_modules += adaptername +builtin_sources += plugins/adaptername.c + +if WIIMOTEPLUGIN +builtin_modules += wiimote +builtin_sources += plugins/wiimote.c +endif + +if MAEMO6PLUGIN +builtin_modules += maemo6 +builtin_sources += plugins/maemo6.c +endif + +if DBUSOOBPLUGIN +builtin_modules += dbusoob +builtin_sources += plugins/dbusoob.c +endif + +if MAINTAINER_MODE +plugin_LTLIBRARIES += plugins/external-dummy.la +plugins_external_dummy_la_SOURCES = plugins/external-dummy.c +plugins_external_dummy_la_LDFLAGS = $(AM_LDFLAGS) -module -avoid-version \ + -no-undefined +plugins_external_dummy_la_CFLAGS = $(AM_CFLAGS) -fvisibility=hidden +endif + +sbin_PROGRAMS += src/bluetoothd + +src_bluetoothd_SOURCES = $(gdbus_sources) $(builtin_sources) \ + $(attrib_sources) $(btio_sources) \ + $(mcap_sources) src/bluetooth.ver \ + src/main.c src/log.h src/log.c \ + src/rfkill.c src/hcid.h src/sdpd.h \ + src/sdpd-server.c src/sdpd-request.c \ + src/sdpd-service.c src/sdpd-database.c \ + src/attrib-server.h src/attrib-server.c \ + src/sdp-xml.h src/sdp-xml.c \ + src/sdp-client.h src/sdp-client.c \ + src/textfile.h src/textfile.c \ + src/glib-helper.h src/glib-helper.c \ + src/oui.h src/oui.c src/uinput.h src/ppoll.h \ + src/plugin.h src/plugin.c \ + src/storage.h src/storage.c \ + src/agent.h src/agent.c \ + src/error.h src/error.c \ + src/manager.h src/manager.c \ + src/adapter.h src/adapter.c \ + src/device.h src/device.c src/attio.h \ + src/dbus-common.c src/dbus-common.h \ + src/event.h src/event.c \ + src/oob.h src/oob.c src/eir.h src/eir.c +src_bluetoothd_LDADD = lib/libbluetooth-private.la @GLIB_LIBS@ @DBUS_LIBS@ \ + -ldl -lrt +src_bluetoothd_LDFLAGS = $(AM_LDFLAGS) -Wl,--export-dynamic \ + -Wl,--version-script=$(srcdir)/src/bluetooth.ver + +src_bluetoothd_DEPENDENCIES = lib/libbluetooth-private.la + +src_bluetoothd_CFLAGS = $(AM_CFLAGS) -DBLUETOOTH_PLUGIN_BUILTIN \ + -DPLUGINDIR=\""$(build_plugindir)"\" +src_bluetoothd_SHORTNAME = bluetoothd + +builtin_files = src/builtin.h $(builtin_nodist) + +nodist_src_bluetoothd_SOURCES = $(builtin_files) + +CLEANFILES += $(builtin_files) + +man_MANS = src/bluetoothd.8 + +if DATAFILES +conf_DATA += src/main.conf +endif + +EXTRA_DIST += src/genbuiltin src/bluetooth.conf src/org.bluez.service \ + src/main.conf network/network.conf \ + input/input.conf serial/serial.conf \ + audio/audio.conf audio/telephony-dummy.c \ + audio/telephony-maemo5.c audio/telephony-ofono.c \ + audio/telephony-maemo6.c sap/sap-dummy.c sap/sap-u8500.c \ + proximity/proximity.conf + +if ALSA +alsadir = $(libdir)/alsa-lib + +alsa_LTLIBRARIES = audio/libasound_module_pcm_bluetooth.la \ + audio/libasound_module_ctl_bluetooth.la + +audio_libasound_module_pcm_bluetooth_la_SOURCES = audio/pcm_bluetooth.c \ + audio/rtp.h audio/ipc.h audio/ipc.c +audio_libasound_module_pcm_bluetooth_la_LDFLAGS = $(AM_LDFLAGS) -module \ + -avoid-version +audio_libasound_module_pcm_bluetooth_la_LIBADD = sbc/libsbc.la \ + lib/libbluetooth-private.la @ALSA_LIBS@ +audio_libasound_module_pcm_bluetooth_la_CFLAGS = $(AM_CFLAGS) @ALSA_CFLAGS@ + +audio_libasound_module_ctl_bluetooth_la_SOURCES = audio/ctl_bluetooth.c \ + audio/rtp.h audio/ipc.h audio/ipc.c +audio_libasound_module_ctl_bluetooth_la_LDFLAGS = $(AM_LDFLAGS) -module \ + -avoid-version +audio_libasound_module_ctl_bluetooth_la_LIBADD = \ + lib/libbluetooth-private.la @ALSA_LIBS@ +audio_libasound_module_ctl_bluetooth_la_CFLAGS = $(AM_CFLAGS) @ALSA_CFLAGS@ + +if DATAFILES +alsaconfdir = $(datadir)/alsa + +alsaconf_DATA = audio/bluetooth.conf +endif +endif + +if AUDIOPLUGIN +if GSTREAMER +gstreamerdir = $(libdir)/gstreamer-0.10 + +gstreamer_LTLIBRARIES = audio/libgstbluetooth.la + +audio_libgstbluetooth_la_SOURCES = audio/gstbluetooth.c audio/gstpragma.h \ + audio/gstsbcenc.h audio/gstsbcenc.c \ + audio/gstsbcdec.h audio/gstsbcdec.c \ + audio/gstsbcparse.h audio/gstsbcparse.c \ + audio/gstavdtpsink.h audio/gstavdtpsink.c \ + audio/gsta2dpsink.h audio/gsta2dpsink.c \ + audio/gstsbcutil.h audio/gstsbcutil.c \ + audio/gstrtpsbcpay.h audio/gstrtpsbcpay.c \ + audio/rtp.h audio/ipc.h audio/ipc.c +audio_libgstbluetooth_la_LDFLAGS = $(AM_LDFLAGS) -module -avoid-version +audio_libgstbluetooth_la_LIBADD = sbc/libsbc.la lib/libbluetooth-private.la \ + @DBUS_LIBS@ @GSTREAMER_LIBS@ \ + -lgstaudio-0.10 -lgstrtp-0.10 +audio_libgstbluetooth_la_CFLAGS = -fvisibility=hidden -fno-strict-aliasing \ + $(AM_CFLAGS) @DBUS_CFLAGS@ @GSTREAMER_CFLAGS@ +endif +endif + +EXTRA_DIST += audio/bluetooth.conf + + +include Makefile.tools + +if DATAFILES +rulesdir = @UDEV_DIR@/rules.d + +udev_files = + +if HID2HCI +udev_files += scripts/bluetooth-hid2hci.rules +endif + +if PCMCIA +udevdir = @UDEV_DIR@ + +udev_files += scripts/bluetooth-serial.rules + +dist_udev_SCRIPTS = scripts/bluetooth_serial +endif + +rules_DATA = $(foreach file,$(udev_files), scripts/97-$(notdir $(file))) +endif + +CLEANFILES += $(rules_DATA) + +EXTRA_DIST += scripts/bluetooth-hid2hci.rules scripts/bluetooth-serial.rules + +EXTRA_DIST += doc/manager-api.txt \ + doc/adapter-api.txt doc/device-api.txt \ + doc/service-api.txt doc/agent-api.txt doc/attribute-api.txt \ + doc/serial-api.txt doc/network-api.txt \ + doc/input-api.txt doc/audio-api.txt doc/control-api.txt \ + doc/hfp-api.txt doc/health-api.txt doc/sap-api.txt \ + doc/media-api.txt doc/assigned-numbers.txt + +AM_YFLAGS = -d + +AM_CFLAGS += @DBUS_CFLAGS@ @GLIB_CFLAGS@ + +INCLUDES = -I$(builddir)/lib -I$(builddir)/src -I$(srcdir)/src \ + -I$(srcdir)/audio -I$(srcdir)/sbc -I$(srcdir)/gdbus \ + -I$(srcdir)/attrib -I$(srcdir)/btio -I$(srcdir)/tools \ + -I$(builddir)/tools -I$(srcdir)/monitor + +if MCAP +INCLUDES += -I$(builddir)/health +endif + +unit_objects = + +if TEST +unit_tests = unit/test-eir + +noinst_PROGRAMS += $(unit_tests) + +unit_test_eir_SOURCES = unit/test-eir.c src/eir.c src/glib-helper.c +unit_test_eir_LDADD = lib/libbluetooth-private.la @GLIB_LIBS@ @CHECK_LIBS@ +unit_test_eir_CFLAGS = $(AM_CFLAGS) @CHECK_CFLAGS@ +unit_objects += $(unit_test_eir_OBJECTS) +else +unit_tests = +endif + +TESTS = $(unit_tests) + +pkgconfigdir = $(libdir)/pkgconfig + +pkgconfig_DATA = bluez.pc + +DISTCHECK_CONFIGURE_FLAGS = --disable-datafiles + +DISTCLEANFILES = $(pkgconfig_DATA) + +MAINTAINERCLEANFILES = Makefile.in \ + aclocal.m4 configure config.h.in config.sub config.guess \ + ltmain.sh depcomp compile missing install-sh mkinstalldirs ylwrap + +src/builtin.h: src/genbuiltin $(builtin_sources) + $(AM_V_GEN)$(srcdir)/src/genbuiltin $(builtin_modules) > $@ + +audio/telephony.c: audio/@TELEPHONY_DRIVER@ + $(AM_V_GEN)$(LN_S) $(abs_top_builddir)/$< $@ + +sap/sap.c: sap/@SAP_DRIVER@ + $(AM_V_GEN)$(LN_S) $(abs_top_srcdir)/$< $@ + +scripts/%.rules: + $(AM_V_GEN)cp $(subst 97-,,$@) $@ + +$(lib_libbluetooth_la_OBJECTS): $(local_headers) + +lib/bluetooth/%.h: lib/%.h + $(AM_V_at)$(MKDIR_P) lib/bluetooth + $(AM_V_GEN)$(LN_S) $(abs_top_builddir)/$< $@ + +clean-local: + $(RM) -r lib/bluetooth diff --git a/Makefile.tools b/Makefile.tools new file mode 100644 index 0000000..5b1efb8 --- /dev/null +++ b/Makefile.tools @@ -0,0 +1,267 @@ + +if TOOLS +if DATAFILES +conf_DATA += tools/rfcomm.conf +endif + +bin_PROGRAMS += tools/rfcomm tools/l2ping \ + tools/hcitool tools/sdptool tools/ciptool + +sbin_PROGRAMS += tools/hciattach tools/hciconfig + +noinst_PROGRAMS += tools/avinfo tools/ppporc \ + tools/hcieventmask tools/hcisecfilter + +tools/kword.c: tools/parser.h + +tools_rfcomm_SOURCES = tools/rfcomm.c tools/parser.y tools/lexer.l \ + tools/kword.h tools/kword.c +EXTRA_tools_rfcomm_SOURCES = tools/parser.h tools/parser.c \ + tools/lexer.c +tools_rfcomm_LDADD = lib/libbluetooth-private.la + +tools_l2ping_LDADD = lib/libbluetooth-private.la + +tools_hciattach_SOURCES = tools/hciattach.c tools/hciattach.h \ + tools/hciattach_st.c \ + tools/hciattach_ti.c \ + tools/hciattach_tialt.c \ + tools/hciattach_ath3k.c \ + tools/hciattach_qualcomm.c \ + tools/hciattach_intel.c +tools_hciattach_LDADD = lib/libbluetooth-private.la + +tools_hciconfig_SOURCES = tools/hciconfig.c tools/csr.h tools/csr.c \ + src/textfile.h src/textfile.c +tools_hciconfig_LDADD = lib/libbluetooth-private.la + +tools_hcitool_SOURCES = tools/hcitool.c src/oui.h src/oui.c \ + src/textfile.h src/textfile.c +tools_hcitool_LDADD = lib/libbluetooth-private.la + +tools_sdptool_SOURCES = tools/sdptool.c src/sdp-xml.h src/sdp-xml.c +tools_sdptool_LDADD = lib/libbluetooth-private.la + +tools_ciptool_LDADD = lib/libbluetooth-private.la + +tools_avinfo_LDADD = lib/libbluetooth-private.la + +tools_ppporc_LDADD = lib/libbluetooth-private.la + +tools_hcieventmask_LDADD = lib/libbluetooth-private.la + +noinst_PROGRAMS += mgmt/btmgmt monitor/btmon emulator/btvirt + +mgmt_btmgmt_SOURCES = mgmt/main.c src/glib-helper.c +mgmt_btmgmt_LDADD = lib/libbluetooth-private.la @GLIB_LIBS@ + +monitor_btmon_SOURCES = monitor/main.c monitor/bt.h \ + monitor/mainloop.h monitor/mainloop.c \ + monitor/hcidump.h monitor/hcidump.c \ + monitor/btsnoop.h monitor/btsnoop.c \ + monitor/control.h monitor/control.c \ + monitor/packet.h monitor/packet.c +monitor_btmon_LDADD = lib/libbluetooth-private.la + +emulator_btvirt_SOURCES = emulator/main.c monitor/bt.h \ + monitor/mainloop.h monitor/mainloop.c \ + emulator/server.h emulator/server.c \ + emulator/vhci.h emulator/vhci.c \ + emulator/btdev.h emulator/btdev.c + +if READLINE +bin_PROGRAMS += attrib/gatttool + +attrib_gatttool_SOURCES = attrib/gatttool.c attrib/att.c attrib/gatt.c \ + attrib/gattrib.c btio/btio.c \ + attrib/gatttool.h attrib/interactive.c \ + attrib/utils.c src/log.c +attrib_gatttool_LDADD = lib/libbluetooth-private.la @GLIB_LIBS@ @READLINE_LIBS@ +endif + +dist_man_MANS += tools/rfcomm.1 tools/l2ping.8 \ + tools/hciattach.8 tools/hciconfig.8 \ + tools/hcitool.1 tools/sdptool.1 tools/ciptool.1 +else +EXTRA_DIST += tools/rfcomm.1 tools/l2ping.8 \ + tools/hciattach.8 tools/hciconfig.8 \ + tools/hcitool.1 tools/sdptool.1 tools/ciptool.1 +endif + +CLEANFILES += tools/lexer.c tools/parser.c tools/parser.h + +EXTRA_DIST += tools/rfcomm.conf + +if BCCMD +sbin_PROGRAMS += tools/bccmd + +tools_bccmd_SOURCES = tools/bccmd.c tools/csr.h tools/csr.c \ + tools/csr_hci.c tools/csr_h4.c tools/csr_3wire.c \ + tools/csr_bcsp.c tools/ubcsp.h tools/ubcsp.c +tools_bccmd_LDADD = lib/libbluetooth-private.la + +if USB +tools_bccmd_SOURCES += tools/csr_usb.c +tools_bccmd_LDADD += @USB_LIBS@ +endif + +dist_man_MANS += tools/bccmd.8 +else +EXTRA_DIST += tools/bccmd.8 +endif + +if HID2HCI +udevdir = @UDEV_DIR@ + +udev_PROGRAMS = tools/hid2hci + +tools_hid2hci_LDADD = @USB_LIBS@ @UDEV_LIBS@ + +dist_man_MANS += tools/hid2hci.8 +else +EXTRA_DIST += tools/hid2hci.8 +endif + +if DFUTOOL +bin_PROGRAMS += tools/dfutool + +tools_dfutool_SOURCES = tools/dfutool.c tools/dfu.h tools/dfu.c +tools_dfutool_LDADD = @USB_LIBS@ + +dist_man_MANS += tools/dfutool.1 +else +EXTRA_DIST += tools/dfutool.1 +endif + + +if USB +noinst_PROGRAMS += tools/dfubabel tools/avctrl + +tools_dfubabel_LDADD = @USB_LIBS@ + +tools_avctrl_LDADD = @USB_LIBS@ +endif + +EXTRA_DIST += tools/dfubabel.1 tools/avctrl.8 + + +if CUPS +cupsdir = $(libdir)/cups/backend + +cups_PROGRAMS = cups/bluetooth + +cups_bluetooth_SOURCES = $(gdbus_sources) cups/main.c cups/cups.h \ + cups/sdp.c cups/spp.c cups/hcrp.c + +cups_bluetooth_LDADD = @GLIB_LIBS@ @DBUS_LIBS@ lib/libbluetooth-private.la +endif + + +if TEST +sbin_PROGRAMS += test/hciemu + +bin_PROGRAMS += test/l2test test/rctest + +noinst_PROGRAMS += test/gaptest test/sdptest test/scotest \ + test/attest test/hstest test/avtest test/ipctest \ + test/lmptest test/bdaddr test/agent \ + test/btiotest test/test-textfile \ + test/uuidtest test/mpris-player + +test_hciemu_LDADD = lib/libbluetooth-private.la + +test_l2test_LDADD = lib/libbluetooth-private.la + +test_rctest_LDADD = lib/libbluetooth-private.la + +test_gaptest_LDADD = @DBUS_LIBS@ + +test_sdptest_LDADD = lib/libbluetooth-private.la + +test_scotest_LDADD = lib/libbluetooth-private.la + +test_attest_LDADD = lib/libbluetooth-private.la + +test_hstest_LDADD = lib/libbluetooth-private.la + +test_avtest_LDADD = lib/libbluetooth-private.la + +test_lmptest_LDADD = lib/libbluetooth-private.la + +test_ipctest_SOURCES = test/ipctest.c audio/ipc.h audio/ipc.c +test_ipctest_LDADD= @GLIB_LIBS@ sbc/libsbc.la + +test_bdaddr_SOURCES = test/bdaddr.c src/oui.h src/oui.c +test_bdaddr_LDADD = lib/libbluetooth-private.la + +test_agent_LDADD = @DBUS_LIBS@ + +test_btiotest_SOURCES = test/btiotest.c btio/btio.h btio/btio.c +test_btiotest_LDADD = @GLIB_LIBS@ lib/libbluetooth-private.la + +test_uuidtest_SOURCES = test/uuidtest.c +test_uuidtest_LDADD = lib/libbluetooth-private.la + +test_mpris_player_SOURCES = test/mpris-player.c +test_mpris_player_LDADD = @DBUS_LIBS@ @GLIB_LIBS@ + +test_test_textfile_SOURCES = test/test-textfile.c src/textfile.h src/textfile.c + +dist_man_MANS += test/rctest.1 test/hciemu.1 + +EXTRA_DIST += test/bdaddr.8 +else +EXTRA_DIST += test/rctest.1 test/hciemu.1 test/bdaddr.8 +endif + +EXTRA_DIST += test/sap-client test/hsplay test/hsmicro \ + test/dbusdef.py test/monitor-bluetooth test/list-devices \ + test/test-discovery test/test-manager test/test-adapter \ + test/test-device test/test-service test/test-serial \ + test/test-telephony test/test-network test/simple-agent \ + test/simple-service test/simple-endpoint test/test-audio \ + test/test-input test/test-sap-server test/test-oob \ + test/test-attrib test/test-proximity test/test-thermometer \ + test/test-serial-proxy test/test-health test/test-health-sink \ + test/service-record.dtd test/service-did.xml \ + test/service-spp.xml test/service-opp.xml test/service-ftp.xml \ + test/simple-player test/test-nap + +if HIDD +bin_PROGRAMS += compat/hidd + +compat_hidd_SOURCES = compat/hidd.c compat/hidd.h src/uinput.h \ + compat/sdp.h compat/sdp.c compat/fakehid.c \ + src/textfile.h src/textfile.c +compat_hidd_LDADD = -lm lib/libbluetooth-private.la + +dist_man_MANS += compat/hidd.1 +else +EXTRA_DIST += compat/hidd.1 +endif + +if PAND +bin_PROGRAMS += compat/pand + +compat_pand_SOURCES = compat/pand.c compat/pand.h \ + compat/bnep.c compat/sdp.h compat/sdp.c \ + src/textfile.h src/textfile.c +compat_pand_LDADD = lib/libbluetooth-private.la + +dist_man_MANS += compat/pand.1 +else +EXTRA_DIST += compat/pand.1 +endif + +if DUND +bin_PROGRAMS += compat/dund + +compat_dund_SOURCES = compat/dund.c compat/dund.h compat/lib.h \ + compat/sdp.h compat/sdp.c compat/dun.c compat/msdun.c \ + src/textfile.h src/textfile.c +compat_dund_LDADD = lib/libbluetooth-private.la + +dist_man_MANS += compat/dund.1 +else +EXTRA_DIST += compat/dund.1 +endif diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..e69de29 diff --git a/README b/README new file mode 100644 index 0000000..77778e0 --- /dev/null +++ b/README @@ -0,0 +1,38 @@ +BlueZ - Bluetooth protocol stack for Linux +****************************************** + +Copyright (C) 2000-2001 Qualcomm Incorporated +Copyright (C) 2002-2003 Maxim Krasnyansky +Copyright (C) 2002-2010 Marcel Holtmann + + +Compilation and installation +============================ + +In order to compile Bluetooth utilities you need following software packages: + - Linux Bluetooth protocol stack (BlueZ) + - GCC compiler + - D-Bus library + - GLib library + - USB library (optional) + - Lexical Analyzer (flex, lex) + - YACC (yacc, bison, byacc) + +To configure run: + ./configure --prefix=/usr --mandir=/usr/share/man \ + --sysconfdir=/etc --localstatedir=/var --libexecdir=/lib + +Configure automatically searches for all required components and packages. + +To compile and install run: + make && make install + + +Information +=========== + +Mailing lists: + linux-bluetooth@vger.kernel.org + +For additional information about the project visit BlueZ web site: + http://www.bluez.org diff --git a/TODO b/TODO new file mode 100644 index 0000000..17e9848 --- /dev/null +++ b/TODO @@ -0,0 +1,229 @@ +Background +========== + +- Priority scale: High, Medium and Low + +- Complexity scale: C1, C2, C4 and C8. The complexity scale is exponential, + with complexity 1 being the lowest complexity. Complexity is a function + of both task 'complexity' and task 'scope'. + + The general rule of thumb is that a complexity 1 task should take 1-2 weeks + for a person very familiar with BlueZ codebase. Higher complexity tasks + require more time and have higher uncertainty. + + Higher complexity tasks should be refined into several lower complexity tasks + once the task is better understood. + +General +========== + +- UUID handling: Use the new functions created for UUID handling in all parts + of BlueZ code. Currently, the new bt_uuid_* functions are being used by + GATT-related code only. + + Priority: high + Complexity: C4 + +- Rename glib-helper file to a more convenient name. The ideia is try to keep + only sdp helpers functions. bt_* prefix shall be also changed. + + Priority: Low + Complexity: C1 + +Low Energy +========== + +- Advertising management. Adapter interface needs to be changed to manage + connection modes, adapter type and advertising policy. See Volume 3, + Part C, section 9.3. If Attribute Server is enabled the LE capable + adapter shall to start advertising. Further investigation is necessary + to define which connectable mode needs to be supported: Non-connectable, + directed connectable and undirected connectable. Basically, two connectable + scenarios shall be addressed: + 1. GATT client is disconnected, but intends to become a Peripheral to + receive indications/notifications. + 2. GATT server intends to accept connections. + + Priority: Medium + Complexity: C2 + +- Define Auto Connection Establishment Procedure. Some profiles such as + Proximity requires an active link to identify path lost situation. It is + necessary to define how to manage connections, it seems that White List + is appropriated to address auto connections, however is not clear if the + this procedure shall be a profile specific detail or if the remote device + object can expose a property "WhiteList", maybe "Trusted" property can be + also used for this purpose. Another alternative is to define a method to + allow application to request/register the wanted scanning/connection + parameters. Before start this task, a RFC/PATCH shall be sent to the ML. + See Volume 3, Part C, section 9.3.5 for more information. + + Priority: Medium + Complexity: C2 + +- Implement a tool(or extend hciconfig) to setup the advertising parameters + and data. Extend hciconfig passing extra arguments when enabling the + advertises is not the right approach, it will be almost impossible to + address all arguments needed in an acceptable way. For testing, we need + a tool to change easily the AD Flags, the UUIDs and other data that can be + exported through the advertising data field. Suggestions: 1) extend hciconfig + passing a config file when enabling advertises; 2) write a ncurses based tool + + Priority: Medium + Complexity: C2 + +- Add new property in the DeviceFound signal to report the device type: + BR/EDR, single mode or dual-mode. + + Priority: Medium + Complexity: C1 + +- Privacy: When privacy is enabled in the adapter, LE scanning/connection + should use a private address. StartDiscovery method shall be changed and + new adapter property shall be added. + + Priority: Medium + Complexity: C1 + +- Static random address setup and storage. Once this address is written + in the a given remote, the address can not be changed anymore. + + Priority: Low + Complexity: C1 + +- Reconnection address: Reconnection address is a non resolvable private + address that the central writes in the peripheral. BlueZ will support + multiple profiles, it is not clear how it needs to be implemented. + Further discussion is necessary. + + Priority: Low + Complexity: C2 + +- Device Name Characteristic is a GAP characteristic for Low Energy. This + characteristic shall be integrated/used in the discovery procedure. The + ideia is to report the value of this characteristic using DeviceFound signals. + Discussion with the community is needed before to start this task. Other GAP + characteristics for LE needs to follow a similar approach. It is not clear + if all GAP characteristics can be exposed using properties instead of a primary + service characteristics. + See Volume 3, Part C, section 12.1 for more information. + + Priority: Low + Complexity: C2 + +ATT/GATT +======== + +- At the moment authentication and authorization is not supported at the + same time, read/write requirements in the attribute server needs to + be extended. According to Bluetooth Specification a server shall check + authentication and authorization requirements before any other check is + performed. + + Priority: Medium + Complexity: C1 + +- ATT/GATT parsing to hcidump. Partially implemented, missing to fix + multiple advertises in the same event and RSSI. + + Priority: Medium + Complexity: C2 + +- Implement ATT PDU validation. Malformed PDUs can cause division by zero + when decoding PDUs. A proper error PDU should be returned for this case. + See decoding function in att.c file. + + Priority: Medium + Complexity: C1 + +- Fix hard-coded PSM for GATT services over basic rate. + + Priority: Low + Complexity: C1 + +- Refactor read_by_group() and read_by_type() in src/attrib-server.c + (they've grown simply too big). First step could be to move out the + long for-loops to new functions called e.g. get_groups() and get_types(). + + Priority: Low + Complexity: C1 + +- Agent for characteristics: Agent interface should be extended to support + authorization per characteristic if the remote is not in the trusted list. + + Priority: Low + Complexity: C1 + +- gatttool should have the ability to wait for req responses before + quitting (some servers require a small sleep even with cmd's). Maybe a + --delay-exit or --timeout command line switch. + + Priority: Low + Complexity: C1 + +- Refactoring of gatt.c functions. Currently, the callbacks of the services + and characteristics discovery functions return the ATT PDU and the caller + needs to call again the same function to fetch the remaining data when + necessary. Investigate if all results can be returned in the callback + result to avoid repeated code. Before change the code, please analyze + if this change will not break the GATT/ATT qualification tests. Maybe + an interactive fetch/query is necessary to pass the tests. + + Priority: Low + Complexity: C1 + +- Client needs to export a property in the Device Characteristic hierarchy + to manage characteristic value changes reports in the remote device. + Currently, Client Characteristic Configuration attribute is not exposed + as an object. The user needs to use gatttool to change the value of the + this attribute to receive notification/indications. Export this attribute + as a property is a proposal that needs further discussion. + + Priority: Low + Complexity: C1 + +- Attribute server should process queued GATT/ATT commands if the + client disconnects. The client can simply send a command and quit, + without wait for a response(ex: Write Command). For this scenario + that the client disconnects the link quickly the queued received + command is ignored. + + Priority: Low + Complecity: C1 + +- Add sdp discovery support to gattool with BR (--sdp, default is 0x1f) + + Priority: Low + Complexity: C1 + +- Implement Server characteristic Configuration support in the attribute + server to manage characteristic value broadcasting. There is a single + instance of the Server Characteristic Configuration for all clients. + See Volume 3, Part G, section 3.3.3.4 for more information. + + Priority: Low + Complexity: C1 + +- Long write is not implemented. Attribute server, client and command line + tool shall be changed to support this feature. + + Priority: Low + Complexity: C2 + +- Define attribute server API. External applications needs to register, + change attributes and to be notified about changes. Example: Proximity, + Time and Alert Profiles. "Local Service hierarchy" in the attribute-api + needs to be proposed and a RFC shall be sent to the ML. + + Priority: Low + Complexity: C2 + Owner: Anderson Lizardo + +Management Interface +==================== + +- Whitelist support (initially only for LE) + + Priority: Medium + Complexity: C2 + Owner: Andre Guedes diff --git a/acinclude.m4 b/acinclude.m4 new file mode 100644 index 0000000..1d6d736 --- /dev/null +++ b/acinclude.m4 @@ -0,0 +1,401 @@ +AC_DEFUN([AC_PROG_CC_PIE], [ + AC_CACHE_CHECK([whether ${CC-cc} accepts -fPIE], ac_cv_prog_cc_pie, [ + echo 'void f(){}' > conftest.c + if test -z "`${CC-cc} -fPIE -pie -c conftest.c 2>&1`"; then + ac_cv_prog_cc_pie=yes + else + ac_cv_prog_cc_pie=no + fi + rm -rf conftest* + ]) +]) + +AC_DEFUN([COMPILER_FLAGS], [ + with_cflags="" + if (test "$USE_MAINTAINER_MODE" = "yes"); then + with_cflags="$with_cflags -Wall -Werror -Wextra" + with_cflags="$with_cflags -Wno-unused-parameter" + with_cflags="$with_cflags -Wno-missing-field-initializers" + with_cflags="$with_cflags -Wdeclaration-after-statement" + with_cflags="$with_cflags -Wmissing-declarations" + with_cflags="$with_cflags -Wredundant-decls" + with_cflags="$with_cflags -Wcast-align" + with_cflags="$with_cflags -DG_DISABLE_DEPRECATED" + fi + + AC_SUBST([WARNING_CFLAGS], $with_cflags) +]) + +AC_DEFUN([AC_FUNC_PPOLL], [ + AC_CHECK_FUNC(ppoll, dummy=yes, AC_DEFINE(NEED_PPOLL, 1, + [Define to 1 if you need the ppoll() function.])) +]) + +AC_DEFUN([AC_INIT_BLUEZ], [ + AC_PREFIX_DEFAULT(/usr/local) + + if (test "${prefix}" = "NONE"); then + dnl no prefix and no sysconfdir, so default to /etc + if (test "$sysconfdir" = '${prefix}/etc'); then + AC_SUBST([sysconfdir], ['/etc']) + fi + + dnl no prefix and no localstatedir, so default to /var + if (test "$localstatedir" = '${prefix}/var'); then + AC_SUBST([localstatedir], ['/var']) + fi + + dnl no prefix and no libexecdir, so default to /lib + if (test "$libexecdir" = '${exec_prefix}/libexec'); then + AC_SUBST([libexecdir], ['/lib']) + fi + + dnl no prefix and no mandir, so use ${prefix}/share/man as default + if (test "$mandir" = '${prefix}/man'); then + AC_SUBST([mandir], ['${prefix}/share/man']) + fi + + prefix="${ac_default_prefix}" + fi + + if (test "${libdir}" = '${exec_prefix}/lib'); then + libdir="${prefix}/lib" + fi + + plugindir="${libdir}/bluetooth/plugins" + + if (test "$sysconfdir" = '${prefix}/etc'); then + configdir="${prefix}/etc/bluetooth" + else + configdir="${sysconfdir}/bluetooth" + fi + + if (test "$localstatedir" = '${prefix}/var'); then + storagedir="${prefix}/var/lib/bluetooth" + else + storagedir="${localstatedir}/lib/bluetooth" + fi + + AC_DEFINE_UNQUOTED(CONFIGDIR, "${configdir}", + [Directory for the configuration files]) + AC_DEFINE_UNQUOTED(STORAGEDIR, "${storagedir}", + [Directory for the storage files]) + + AC_SUBST(CONFIGDIR, "${configdir}") + AC_SUBST(STORAGEDIR, "${storagedir}") + + UDEV_DIR="`$PKG_CONFIG --variable=udevdir udev`" + if (test -z "${UDEV_DIR}"); then + UDEV_DIR="/lib/udev" + fi + AC_SUBST(UDEV_DIR) +]) + +AC_DEFUN([AC_PATH_DBUS], [ + PKG_CHECK_MODULES(DBUS, dbus-1 >= 1.4, dummy=yes, + AC_MSG_ERROR(D-Bus >= 1.4 is required)) + AC_SUBST(DBUS_CFLAGS) + AC_SUBST(DBUS_LIBS) +]) + +AC_DEFUN([AC_PATH_GLIB], [ + PKG_CHECK_MODULES(GLIB, glib-2.0 >= 2.28, dummy=yes, + AC_MSG_ERROR(GLib >= 2.28 is required)) + AC_SUBST(GLIB_CFLAGS) + AC_SUBST(GLIB_LIBS) +]) + +AC_DEFUN([AC_PATH_GSTREAMER], [ + PKG_CHECK_MODULES(GSTREAMER, gstreamer-0.10 >= 0.10.30 gstreamer-plugins-base-0.10, gstreamer_found=yes, + AC_MSG_WARN(GStreamer library version 0.10.30 or later is required);gstreamer_found=no) + AC_SUBST(GSTREAMER_CFLAGS) + AC_SUBST(GSTREAMER_LIBS) + GSTREAMER_PLUGINSDIR=`$PKG_CONFIG --variable=pluginsdir gstreamer-0.10` + AC_SUBST(GSTREAMER_PLUGINSDIR) +]) + +AC_DEFUN([AC_PATH_ALSA], [ + PKG_CHECK_MODULES(ALSA, alsa, alsa_found=yes, alsa_found=no) + AC_CHECK_LIB(rt, clock_gettime, ALSA_LIBS="$ALSA_LIBS -lrt", alsa_found=no) + AC_SUBST(ALSA_CFLAGS) + AC_SUBST(ALSA_LIBS) +]) + +AC_DEFUN([AC_PATH_USB], [ + PKG_CHECK_MODULES(USB, libusb, usb_found=yes, usb_found=no) + AC_SUBST(USB_CFLAGS) + AC_SUBST(USB_LIBS) + AC_CHECK_LIB(usb, usb_get_busses, dummy=yes, + AC_DEFINE(NEED_USB_GET_BUSSES, 1, + [Define to 1 if you need the usb_get_busses() function.])) + AC_CHECK_LIB(usb, usb_interrupt_read, dummy=yes, + AC_DEFINE(NEED_USB_INTERRUPT_READ, 1, + [Define to 1 if you need the usb_interrupt_read() function.])) +]) + +AC_DEFUN([AC_PATH_UDEV], [ + PKG_CHECK_MODULES(UDEV, libudev, udev_found=yes, udev_found=no) + AC_SUBST(UDEV_CFLAGS) + AC_SUBST(UDEV_LIBS) +]) + +AC_DEFUN([AC_PATH_SNDFILE], [ + PKG_CHECK_MODULES(SNDFILE, sndfile, sndfile_found=yes, sndfile_found=no) + AC_SUBST(SNDFILE_CFLAGS) + AC_SUBST(SNDFILE_LIBS) +]) + +AC_DEFUN([AC_PATH_READLINE], [ + AC_CHECK_HEADER(readline/readline.h, + AC_CHECK_LIB(readline, main, + [ readline_found=yes + AC_SUBST(READLINE_LIBS, "-lreadline") + ], readline_found=no), + []) +]) + +AC_DEFUN([AC_PATH_CHECK], [ + PKG_CHECK_MODULES(CHECK, check >= 0.9.6, check_found=yes, check_found=no) + AC_SUBST(CHECK_CFLAGS) + AC_SUBST(CHECK_LIBS) +]) + +AC_DEFUN([AC_PATH_OUI], [ + AC_ARG_WITH(ouifile, + AS_HELP_STRING([--with-ouifile=PATH],[Path to the oui.txt file @<:@auto@:>@]), + [ac_with_ouifile=$withval], + [ac_with_ouifile="/var/lib/misc/oui.txt"]) + AC_DEFINE_UNQUOTED(OUIFILE, ["$ac_with_ouifile"], [Define the OUI file path]) +]) + +AC_DEFUN([AC_ARG_BLUEZ], [ + debug_enable=no + optimization_enable=yes + fortify_enable=yes + pie_enable=yes + sndfile_enable=${sndfile_found} + hal_enable=no + usb_enable=${usb_found} + alsa_enable=${alsa_found} + gstreamer_enable=${gstreamer_found} + audio_enable=yes + input_enable=yes + serial_enable=yes + network_enable=yes + sap_enable=no + service_enable=yes + health_enable=no + pnat_enable=no + tools_enable=yes + hidd_enable=no + pand_enable=no + dund_enable=no + cups_enable=no + test_enable=no + bccmd_enable=no + pcmcia_enable=no + hid2hci_enable=no + dfutool_enable=no + datafiles_enable=yes + telephony_driver=dummy + maemo6_enable=no + sap_driver=dummy + dbusoob_enable=no + wiimote_enable=no + gatt_enable=no + + AC_ARG_ENABLE(optimization, AC_HELP_STRING([--disable-optimization], [disable code optimization]), [ + optimization_enable=${enableval} + ]) + + AC_ARG_ENABLE(fortify, AC_HELP_STRING([--disable-fortify], [disable compile time buffer checks]), [ + fortify_enable=${enableval} + ]) + + AC_ARG_ENABLE(pie, AC_HELP_STRING([--disable-pie], [disable position independent executables flag]), [ + pie_enable=${enableval} + ]) + + AC_ARG_ENABLE(network, AC_HELP_STRING([--disable-network], [disable network plugin]), [ + network_enable=${enableval} + ]) + + AC_ARG_ENABLE(sap, AC_HELP_STRING([--enable-sap], [enable sap plugin]), [ + sap_enable=${enableval} + ]) + + AC_ARG_WITH(sap, AC_HELP_STRING([--with-sap=DRIVER], [select SAP driver]), [ + sap_driver=${withval} + ]) + AC_SUBST([SAP_DRIVER], [sap-${sap_driver}.c]) + + AC_ARG_ENABLE(serial, AC_HELP_STRING([--disable-serial], [disable serial plugin]), [ + serial_enable=${enableval} + ]) + + AC_ARG_ENABLE(input, AC_HELP_STRING([--disable-input], [disable input plugin]), [ + input_enable=${enableval} + ]) + + AC_ARG_ENABLE(audio, AC_HELP_STRING([--disable-audio], [disable audio plugin]), [ + audio_enable=${enableval} + ]) + + AC_ARG_ENABLE(service, AC_HELP_STRING([--disable-service], [disable service plugin]), [ + service_enable=${enableval} + ]) + + AC_ARG_ENABLE(health, AC_HELP_STRING([--enable-health], [enable health plugin]), [ + health_enable=${enableval} + ]) + + AC_ARG_ENABLE(pnat, AC_HELP_STRING([--enable-pnat], [enable pnat plugin]), [ + pnat_enable=${enableval} + ]) + + AC_ARG_ENABLE(gstreamer, AC_HELP_STRING([--enable-gstreamer], [enable GStreamer support]), [ + gstreamer_enable=${enableval} + ]) + + AC_ARG_ENABLE(alsa, AC_HELP_STRING([--enable-alsa], [enable ALSA support]), [ + alsa_enable=${enableval} + ]) + + AC_ARG_ENABLE(usb, AC_HELP_STRING([--enable-usb], [enable USB support]), [ + usb_enable=${enableval} + ]) + + AC_ARG_ENABLE(tools, AC_HELP_STRING([--enable-tools], [install Bluetooth utilities]), [ + tools_enable=${enableval} + ]) + + AC_ARG_ENABLE(bccmd, AC_HELP_STRING([--enable-bccmd], [install BCCMD interface utility]), [ + bccmd_enable=${enableval} + ]) + + AC_ARG_ENABLE(pcmcia, AC_HELP_STRING([--enable-pcmcia], [install PCMCIA serial script]), [ + pcmcia_enable=${enableval} + ]) + + AC_ARG_ENABLE(hid2hci, AC_HELP_STRING([--enable-hid2hci], [install HID mode switching utility]), [ + hid2hci_enable=${enableval} + ]) + + AC_ARG_ENABLE(dfutool, AC_HELP_STRING([--enable-dfutool], [install DFU firmware upgrade utility]), [ + dfutool_enable=${enableval} + ]) + + AC_ARG_ENABLE(hidd, AC_HELP_STRING([--enable-hidd], [install HID daemon]), [ + hidd_enable=${enableval} + ]) + + AC_ARG_ENABLE(pand, AC_HELP_STRING([--enable-pand], [install PAN daemon]), [ + pand_enable=${enableval} + ]) + + AC_ARG_ENABLE(dund, AC_HELP_STRING([--enable-dund], [install DUN daemon]), [ + dund_enable=${enableval} + ]) + + AC_ARG_ENABLE(cups, AC_HELP_STRING([--enable-cups], [install CUPS backend support]), [ + cups_enable=${enableval} + ]) + + AC_ARG_ENABLE(test, AC_HELP_STRING([--enable-test], [install test programs]), [ + test_enable=${enableval} + ]) + + AC_ARG_ENABLE(datafiles, AC_HELP_STRING([--enable-datafiles], [install Bluetooth configuration and data files]), [ + datafiles_enable=${enableval} + ]) + + AC_ARG_ENABLE(debug, AC_HELP_STRING([--enable-debug], [enable compiling with debugging information]), [ + debug_enable=${enableval} + ]) + + AC_ARG_WITH(telephony, AC_HELP_STRING([--with-telephony=DRIVER], [select telephony driver]), [ + telephony_driver=${withval} + ]) + + AC_SUBST([TELEPHONY_DRIVER], [telephony-${telephony_driver}.c]) + + AC_ARG_ENABLE(maemo6, AC_HELP_STRING([--enable-maemo6], [compile with maemo6 plugin]), [ + maemo6_enable=${enableval} + ]) + + AC_ARG_ENABLE(dbusoob, AC_HELP_STRING([--enable-dbusoob], [compile with D-Bus OOB plugin]), [ + dbusoob_enable=${enableval} + ]) + + AC_ARG_ENABLE(wiimote, AC_HELP_STRING([--enable-wiimote], [compile with Wii Remote plugin]), [ + wiimote_enable=${enableval} + ]) + + AC_ARG_ENABLE(hal, AC_HELP_STRING([--enable-hal], [Use HAL to determine adapter class]), [ + hal_enable=${enableval} + ]) + + AC_ARG_ENABLE(gatt, AC_HELP_STRING([--enable-gatt], [enable gatt module]), [ + gatt_enable=${enableval} + ]) + + misc_cflags="" + misc_ldflags="" + + if (test "${fortify_enable}" = "yes"); then + misc_cflags="$misc_cflags -D_FORTIFY_SOURCE=2" + fi + + if (test "${pie_enable}" = "yes" && test "${ac_cv_prog_cc_pie}" = "yes"); then + misc_cflags="$misc_cflags -fPIC" + misc_ldflags="$misc_ldflags -pie" + fi + + if (test "${debug_enable}" = "yes" && test "${ac_cv_prog_cc_g}" = "yes"); then + misc_cflags="$misc_cflags -g" + fi + + if (test "${optimization_enable}" = "no"); then + misc_cflags="$misc_cflags -O0" + fi + + AC_SUBST([MISC_CFLAGS], $misc_cflags) + AC_SUBST([MISC_LDFLAGS], $misc_ldflags) + + if (test "${usb_enable}" = "yes" && test "${usb_found}" = "yes"); then + AC_DEFINE(HAVE_LIBUSB, 1, [Define to 1 if you have USB library.]) + fi + + AM_CONDITIONAL(SNDFILE, test "${sndfile_enable}" = "yes" && test "${sndfile_found}" = "yes") + AM_CONDITIONAL(USB, test "${usb_enable}" = "yes" && test "${usb_found}" = "yes") + AM_CONDITIONAL(SBC, test "${alsa_enable}" = "yes" || test "${gstreamer_enable}" = "yes" || + test "${test_enable}" = "yes") + AM_CONDITIONAL(ALSA, test "${alsa_enable}" = "yes" && test "${alsa_found}" = "yes") + AM_CONDITIONAL(GSTREAMER, test "${gstreamer_enable}" = "yes" && test "${gstreamer_found}" = "yes") + AM_CONDITIONAL(AUDIOPLUGIN, test "${audio_enable}" = "yes") + AM_CONDITIONAL(INPUTPLUGIN, test "${input_enable}" = "yes") + AM_CONDITIONAL(SERIALPLUGIN, test "${serial_enable}" = "yes") + AM_CONDITIONAL(NETWORKPLUGIN, test "${network_enable}" = "yes") + AM_CONDITIONAL(SAPPLUGIN, test "${sap_enable}" = "yes") + AM_CONDITIONAL(SERVICEPLUGIN, test "${service_enable}" = "yes") + AM_CONDITIONAL(HEALTHPLUGIN, test "${health_enable}" = "yes") + AM_CONDITIONAL(MCAP, test "${health_enable}" = "yes") + AM_CONDITIONAL(HAL, test "${hal_enable}" = "yes") + AM_CONDITIONAL(READLINE, test "${readline_found}" = "yes") + AM_CONDITIONAL(PNATPLUGIN, test "${pnat_enable}" = "yes") + AM_CONDITIONAL(HIDD, test "${hidd_enable}" = "yes") + AM_CONDITIONAL(PAND, test "${pand_enable}" = "yes") + AM_CONDITIONAL(DUND, test "${dund_enable}" = "yes") + AM_CONDITIONAL(CUPS, test "${cups_enable}" = "yes") + AM_CONDITIONAL(TEST, test "${test_enable}" = "yes" && test "${check_found}" = "yes") + AM_CONDITIONAL(TOOLS, test "${tools_enable}" = "yes") + AM_CONDITIONAL(BCCMD, test "${bccmd_enable}" = "yes") + AM_CONDITIONAL(PCMCIA, test "${pcmcia_enable}" = "yes") + AM_CONDITIONAL(HID2HCI, test "${hid2hci_enable}" = "yes" && test "${usb_found}" = "yes" && test "${udev_found}" = "yes") + AM_CONDITIONAL(DFUTOOL, test "${dfutool_enable}" = "yes" && test "${usb_found}" = "yes") + AM_CONDITIONAL(DATAFILES, test "${datafiles_enable}" = "yes") + AM_CONDITIONAL(MAEMO6PLUGIN, test "${maemo6_enable}" = "yes") + AM_CONDITIONAL(DBUSOOBPLUGIN, test "${dbusoob_enable}" = "yes") + AM_CONDITIONAL(WIIMOTEPLUGIN, test "${wiimote_enable}" = "yes") + AM_CONDITIONAL(GATTMODULES, test "${gatt_enable}" = "yes") +]) diff --git a/alert/main.c b/alert/main.c new file mode 100644 index 0000000..ec4ab6d --- /dev/null +++ b/alert/main.c @@ -0,0 +1,58 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2011 Nokia Corporation + * Copyright (C) 2011 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include + +#include "plugin.h" +#include "hcid.h" +#include "log.h" +#include "server.h" + +static int alert_init(void) +{ + if (!main_opts.gatt_enabled) { + DBG("GATT is disabled"); + return -ENOTSUP; + } + + return alert_server_init(); +} + +static void alert_exit(void) +{ + if (!main_opts.gatt_enabled) + return; + + alert_server_exit(); +} + +BLUETOOTH_PLUGIN_DEFINE(alert, VERSION, + BLUETOOTH_PLUGIN_PRIORITY_DEFAULT, + alert_init, alert_exit) diff --git a/alert/server.c b/alert/server.c new file mode 100644 index 0000000..d91b156 --- /dev/null +++ b/alert/server.c @@ -0,0 +1,38 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2011 Nokia Corporation + * Copyright (C) 2011 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "server.h" + +int alert_server_init(void) +{ + return 0; +} + +void alert_server_exit(void) +{ +} diff --git a/alert/server.h b/alert/server.h new file mode 100644 index 0000000..e59bdb1 --- /dev/null +++ b/alert/server.h @@ -0,0 +1,26 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2011 Nokia Corporation + * Copyright (C) 2011 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +int alert_server_init(void); +void alert_server_exit(void); diff --git a/attrib/att-database.h b/attrib/att-database.h new file mode 100644 index 0000000..3e854aa --- /dev/null +++ b/attrib/att-database.h @@ -0,0 +1,43 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2012 Texas Instruments Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +/* Requirements for read/write operations */ +enum { + ATT_NONE, /* No restrictions */ + ATT_AUTHENTICATION, /* Authentication required */ + ATT_AUTHORIZATION, /* Authorization required */ + ATT_NOT_PERMITTED, /* Operation not permitted */ +}; + +struct attribute { + uint16_t handle; + bt_uuid_t uuid; + int read_reqs; + int write_reqs; + uint8_t (*read_cb)(struct attribute *a, struct btd_device *device, + gpointer user_data); + uint8_t (*write_cb)(struct attribute *a, struct btd_device *device, + gpointer user_data); + gpointer cb_user_data; + int len; + uint8_t *data; +}; diff --git a/attrib/att.c b/attrib/att.c new file mode 100644 index 0000000..c8e2e1d --- /dev/null +++ b/attrib/att.c @@ -0,0 +1,975 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2010 Nokia Corporation + * Copyright (C) 2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include +#include + +#include +#include + +#include + +#include "att.h" + +const char *att_ecode2str(uint8_t status) +{ + switch (status) { + case ATT_ECODE_INVALID_HANDLE: + return "Invalid handle"; + case ATT_ECODE_READ_NOT_PERM: + return "Attribute can't be read"; + case ATT_ECODE_WRITE_NOT_PERM: + return "Attribute can't be written"; + case ATT_ECODE_INVALID_PDU: + return "Attribute PDU was invalid"; + case ATT_ECODE_AUTHENTICATION: + return "Attribute requires authentication before read/write"; + case ATT_ECODE_REQ_NOT_SUPP: + return "Server doesn't support the request received"; + case ATT_ECODE_INVALID_OFFSET: + return "Offset past the end of the attribute"; + case ATT_ECODE_AUTHORIZATION: + return "Attribute requires authorization before read/write"; + case ATT_ECODE_PREP_QUEUE_FULL: + return "Too many prepare writes have been queued"; + case ATT_ECODE_ATTR_NOT_FOUND: + return "No attribute found within the given range"; + case ATT_ECODE_ATTR_NOT_LONG: + return "Attribute can't be read/written using Read Blob Req"; + case ATT_ECODE_INSUFF_ENCR_KEY_SIZE: + return "Encryption Key Size is insufficient"; + case ATT_ECODE_INVAL_ATTR_VALUE_LEN: + return "Attribute value length is invalid"; + case ATT_ECODE_UNLIKELY: + return "Request attribute has encountered an unlikely error"; + case ATT_ECODE_INSUFF_ENC: + return "Encryption required before read/write"; + case ATT_ECODE_UNSUPP_GRP_TYPE: + return "Attribute type is not a supported grouping attribute"; + case ATT_ECODE_INSUFF_RESOURCES: + return "Insufficient Resources to complete the request"; + case ATT_ECODE_IO: + return "Internal application error: I/O"; + case ATT_ECODE_TIMEOUT: + return "A timeout occured"; + case ATT_ECODE_ABORTED: + return "The operation was aborted"; + default: + return "Unexpected error code"; + } +} + +void att_data_list_free(struct att_data_list *list) +{ + if (list == NULL) + return; + + if (list->data) { + int i; + for (i = 0; i < list->num; i++) + g_free(list->data[i]); + } + + g_free(list->data); + g_free(list); +} + +struct att_data_list *att_data_list_alloc(uint16_t num, uint16_t len) +{ + struct att_data_list *list; + int i; + + list = g_new0(struct att_data_list, 1); + list->len = len; + list->num = num; + + list->data = g_malloc0(sizeof(uint8_t *) * num); + + for (i = 0; i < num; i++) + list->data[i] = g_malloc0(sizeof(uint8_t) * len); + + return list; +} + +uint16_t enc_read_by_grp_req(uint16_t start, uint16_t end, bt_uuid_t *uuid, + uint8_t *pdu, int len) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(start) + sizeof(end); + uint16_t length; + + if (!uuid) + return 0; + + if (uuid->type == BT_UUID16) + length = 2; + else if (uuid->type == BT_UUID128) + length = 16; + else + return 0; + + if (len < min_len + length) + return 0; + + pdu[0] = ATT_OP_READ_BY_GROUP_REQ; + att_put_u16(start, &pdu[1]); + att_put_u16(end, &pdu[3]); + + att_put_uuid(*uuid, &pdu[5]); + + return min_len + length; +} + +uint16_t dec_read_by_grp_req(const uint8_t *pdu, int len, uint16_t *start, + uint16_t *end, bt_uuid_t *uuid) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(*start) + sizeof(*end); + + if (pdu == NULL) + return 0; + + if (start == NULL || end == NULL || uuid == NULL) + return 0; + + if (pdu[0] != ATT_OP_READ_BY_GROUP_REQ) + return 0; + + if (len < min_len + 2) + return 0; + + *start = att_get_u16(&pdu[1]); + *end = att_get_u16(&pdu[3]); + if (len == min_len + 2) + *uuid = att_get_uuid16(&pdu[5]); + else + *uuid = att_get_uuid128(&pdu[5]); + + return len; +} + +uint16_t enc_read_by_grp_resp(struct att_data_list *list, uint8_t *pdu, + int len) +{ + int i; + uint16_t w; + uint8_t *ptr; + + if (list == NULL) + return 0; + + if (len < list->len + 2) + return 0; + + pdu[0] = ATT_OP_READ_BY_GROUP_RESP; + pdu[1] = list->len; + + ptr = &pdu[2]; + + for (i = 0, w = 2; i < list->num && w + list->len <= len; i++) { + memcpy(ptr, list->data[i], list->len); + ptr += list->len; + w += list->len; + } + + return w; +} + +struct att_data_list *dec_read_by_grp_resp(const uint8_t *pdu, int len) +{ + struct att_data_list *list; + const uint8_t *ptr; + uint16_t elen, num; + int i; + + if (pdu[0] != ATT_OP_READ_BY_GROUP_RESP) + return NULL; + + elen = pdu[1]; + num = (len - 2) / elen; + list = att_data_list_alloc(num, elen); + + ptr = &pdu[2]; + + for (i = 0; i < num; i++) { + memcpy(list->data[i], ptr, list->len); + ptr += list->len; + } + + return list; +} + +uint16_t enc_find_by_type_req(uint16_t start, uint16_t end, bt_uuid_t *uuid, + const uint8_t *value, int vlen, uint8_t *pdu, int len) +{ + uint16_t min_len = sizeof(pdu[0]) + sizeof(start) + sizeof(end) + + sizeof(uint16_t); + + if (pdu == NULL) + return 0; + + if (!uuid) + return 0; + + if (uuid->type != BT_UUID16) + return 0; + + if (len < min_len) + return 0; + + if (vlen > len - min_len) + vlen = len - min_len; + + pdu[0] = ATT_OP_FIND_BY_TYPE_REQ; + att_put_u16(start, &pdu[1]); + att_put_u16(end, &pdu[3]); + att_put_uuid16(*uuid, &pdu[5]); + + if (vlen > 0) { + memcpy(&pdu[7], value, vlen); + return min_len + vlen; + } + + return min_len; +} + +uint16_t dec_find_by_type_req(const uint8_t *pdu, int len, uint16_t *start, + uint16_t *end, bt_uuid_t *uuid, uint8_t *value, int *vlen) +{ + int valuelen; + uint16_t min_len = sizeof(pdu[0]) + sizeof(*start) + + sizeof(*end) + sizeof(uint16_t); + + if (pdu == NULL) + return 0; + + if (len < min_len) + return 0; + + if (pdu[0] != ATT_OP_FIND_BY_TYPE_REQ) + return 0; + + /* First requested handle number */ + if (start) + *start = att_get_u16(&pdu[1]); + + /* Last requested handle number */ + if (end) + *end = att_get_u16(&pdu[3]); + + /* Always UUID16 */ + if (uuid) + *uuid = att_get_uuid16(&pdu[5]); + + valuelen = len - min_len; + + /* Attribute value to find */ + if (valuelen > 0 && value) + memcpy(value, pdu + min_len, valuelen); + + if (vlen) + *vlen = valuelen; + + return len; +} + +uint16_t enc_find_by_type_resp(GSList *matches, uint8_t *pdu, int len) +{ + GSList *l; + uint16_t offset; + + if (pdu == NULL || len < 5) + return 0; + + pdu[0] = ATT_OP_FIND_BY_TYPE_RESP; + + for (l = matches, offset = 1; l && len >= (offset + 4); + l = l->next, offset += 4) { + struct att_range *range = l->data; + + att_put_u16(range->start, &pdu[offset]); + att_put_u16(range->end, &pdu[offset + 2]); + } + + return offset; +} + +GSList *dec_find_by_type_resp(const uint8_t *pdu, int len) +{ + struct att_range *range; + GSList *matches; + int offset; + + if (pdu == NULL || len < 5) + return NULL; + + if (pdu[0] != ATT_OP_FIND_BY_TYPE_RESP) + return NULL; + + for (offset = 1, matches = NULL; len >= (offset + 4); offset += 4) { + range = g_new0(struct att_range, 1); + range->start = att_get_u16(&pdu[offset]); + range->end = att_get_u16(&pdu[offset + 2]); + + matches = g_slist_append(matches, range); + } + + return matches; +} + +uint16_t enc_read_by_type_req(uint16_t start, uint16_t end, bt_uuid_t *uuid, + uint8_t *pdu, int len) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(start) + sizeof(end); + uint16_t length; + + if (!uuid) + return 0; + + if (uuid->type == BT_UUID16) + length = 2; + else if (uuid->type == BT_UUID128) + length = 16; + else + return 0; + + if (len < min_len + length) + return 0; + + pdu[0] = ATT_OP_READ_BY_TYPE_REQ; + att_put_u16(start, &pdu[1]); + att_put_u16(end, &pdu[3]); + + att_put_uuid(*uuid, &pdu[5]); + + return min_len + length; +} + +uint16_t dec_read_by_type_req(const uint8_t *pdu, int len, uint16_t *start, + uint16_t *end, bt_uuid_t *uuid) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(*start) + sizeof(*end); + + if (pdu == NULL) + return 0; + + if (start == NULL || end == NULL || uuid == NULL) + return 0; + + if (len < min_len + 2) + return 0; + + if (pdu[0] != ATT_OP_READ_BY_TYPE_REQ) + return 0; + + *start = att_get_u16(&pdu[1]); + *end = att_get_u16(&pdu[3]); + + if (len == min_len + 2) + *uuid = att_get_uuid16(&pdu[5]); + else + *uuid = att_get_uuid128(&pdu[5]); + + return len; +} + +uint16_t enc_read_by_type_resp(struct att_data_list *list, uint8_t *pdu, int len) +{ + uint8_t *ptr; + int i, w, l; + + if (list == NULL) + return 0; + + if (pdu == NULL) + return 0; + + l = MIN(len - 2, list->len); + + pdu[0] = ATT_OP_READ_BY_TYPE_RESP; + pdu[1] = l; + ptr = &pdu[2]; + + for (i = 0, w = 2; i < list->num && w + l <= len; i++) { + memcpy(ptr, list->data[i], l); + ptr += l; + w += l; + } + + return w; +} + +struct att_data_list *dec_read_by_type_resp(const uint8_t *pdu, int len) +{ + struct att_data_list *list; + const uint8_t *ptr; + uint16_t elen, num; + int i; + + if (pdu[0] != ATT_OP_READ_BY_TYPE_RESP) + return NULL; + + elen = pdu[1]; + num = (len - 2) / elen; + list = att_data_list_alloc(num, elen); + + ptr = &pdu[2]; + + for (i = 0; i < num; i++) { + memcpy(list->data[i], ptr, list->len); + ptr += list->len; + } + + return list; +} + +uint16_t enc_write_cmd(uint16_t handle, const uint8_t *value, int vlen, + uint8_t *pdu, int len) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(handle); + + if (pdu == NULL) + return 0; + + if (len < min_len) + return 0; + + if (vlen > len - min_len) + vlen = len - min_len; + + pdu[0] = ATT_OP_WRITE_CMD; + att_put_u16(handle, &pdu[1]); + + if (vlen > 0) { + memcpy(&pdu[3], value, vlen); + return min_len + vlen; + } + + return min_len; +} + +uint16_t dec_write_cmd(const uint8_t *pdu, int len, uint16_t *handle, + uint8_t *value, int *vlen) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(*handle); + + if (pdu == NULL) + return 0; + + if (value == NULL || vlen == NULL || handle == NULL) + return 0; + + if (len < min_len) + return 0; + + if (pdu[0] != ATT_OP_WRITE_CMD) + return 0; + + *handle = att_get_u16(&pdu[1]); + memcpy(value, pdu + min_len, len - min_len); + *vlen = len - min_len; + + return len; +} + +uint16_t enc_write_req(uint16_t handle, const uint8_t *value, int vlen, + uint8_t *pdu, int len) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(handle); + + if (pdu == NULL) + return 0; + + if (len < min_len) + return 0; + + if (vlen > len - min_len) + vlen = len - min_len; + + pdu[0] = ATT_OP_WRITE_REQ; + att_put_u16(handle, &pdu[1]); + + if (vlen > 0) { + memcpy(&pdu[3], value, vlen); + return min_len + vlen; + } + + return min_len; +} + +uint16_t dec_write_req(const uint8_t *pdu, int len, uint16_t *handle, + uint8_t *value, int *vlen) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(*handle); + + if (pdu == NULL) + return 0; + + if (value == NULL || vlen == NULL || handle == NULL) + return 0; + + if (len < min_len) + return 0; + + if (pdu[0] != ATT_OP_WRITE_REQ) + return 0; + + *handle = att_get_u16(&pdu[1]); + *vlen = len - min_len; + if (*vlen > 0) + memcpy(value, pdu + min_len, *vlen); + + return len; +} + +uint16_t enc_write_resp(uint8_t *pdu, int len) +{ + if (pdu == NULL) + return 0; + + pdu[0] = ATT_OP_WRITE_RESP; + + return sizeof(pdu[0]); +} + +uint16_t dec_write_resp(const uint8_t *pdu, int len) +{ + if (pdu == NULL) + return 0; + + if (pdu[0] != ATT_OP_WRITE_RESP) + return 0; + + return len; +} + +uint16_t enc_read_req(uint16_t handle, uint8_t *pdu, int len) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(handle); + + if (pdu == NULL) + return 0; + + if (len < min_len) + return 0; + + pdu[0] = ATT_OP_READ_REQ; + att_put_u16(handle, &pdu[1]); + + return min_len; +} + +uint16_t enc_read_blob_req(uint16_t handle, uint16_t offset, uint8_t *pdu, + int len) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(handle) + + sizeof(offset); + + if (pdu == NULL) + return 0; + + if (len < min_len) + return 0; + + pdu[0] = ATT_OP_READ_BLOB_REQ; + att_put_u16(handle, &pdu[1]); + att_put_u16(offset, &pdu[3]); + + return min_len; +} + +uint16_t dec_read_req(const uint8_t *pdu, int len, uint16_t *handle) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(*handle); + + if (pdu == NULL) + return 0; + + if (handle == NULL) + return 0; + + if (len < min_len) + return 0; + + if (pdu[0] != ATT_OP_READ_REQ) + return 0; + + *handle = att_get_u16(&pdu[1]); + + return min_len; +} + +uint16_t dec_read_blob_req(const uint8_t *pdu, int len, uint16_t *handle, + uint16_t *offset) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(*handle) + + sizeof(*offset); + + if (pdu == NULL) + return 0; + + if (handle == NULL) + return 0; + + if (offset == NULL) + return 0; + + if (len < min_len) + return 0; + + if (pdu[0] != ATT_OP_READ_BLOB_REQ) + return 0; + + *handle = att_get_u16(&pdu[1]); + *offset = att_get_u16(&pdu[3]); + + return min_len; +} + +uint16_t enc_read_resp(uint8_t *value, int vlen, uint8_t *pdu, int len) +{ + if (pdu == NULL) + return 0; + + /* If the attribute value length is longer than the allowed PDU size, + * send only the octets that fit on the PDU. The remaining octets can + * be requested using the Read Blob Request. */ + if (vlen > len - 1) + vlen = len - 1; + + pdu[0] = ATT_OP_READ_RESP; + + memcpy(pdu + 1, value, vlen); + + return vlen + 1; +} + +uint16_t enc_read_blob_resp(uint8_t *value, int vlen, uint16_t offset, + uint8_t *pdu, int len) +{ + if (pdu == NULL) + return 0; + + vlen -= offset; + if (vlen > len - 1) + vlen = len - 1; + + pdu[0] = ATT_OP_READ_BLOB_RESP; + + memcpy(pdu + 1, &value[offset], vlen); + + return vlen + 1; +} + +uint16_t dec_read_resp(const uint8_t *pdu, int len, uint8_t *value, int *vlen) +{ + if (pdu == NULL) + return 0; + + if (value == NULL || vlen == NULL) + return 0; + + if (pdu[0] != ATT_OP_READ_RESP) + return 0; + + memcpy(value, pdu + 1, len - 1); + + *vlen = len - 1; + + return len; +} + +uint16_t enc_error_resp(uint8_t opcode, uint16_t handle, uint8_t status, + uint8_t *pdu, int len) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(opcode) + + sizeof(handle) + sizeof(status); + uint16_t u16; + + if (len < min_len) + return 0; + + u16 = htobs(handle); + pdu[0] = ATT_OP_ERROR; + pdu[1] = opcode; + memcpy(&pdu[2], &u16, sizeof(u16)); + pdu[4] = status; + + return min_len; +} + +uint16_t enc_find_info_req(uint16_t start, uint16_t end, uint8_t *pdu, int len) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(start) + sizeof(end); + + if (pdu == NULL) + return 0; + + if (len < min_len) + return 0; + + pdu[0] = ATT_OP_FIND_INFO_REQ; + att_put_u16(start, &pdu[1]); + att_put_u16(end, &pdu[3]); + + return min_len; +} + +uint16_t dec_find_info_req(const uint8_t *pdu, int len, uint16_t *start, + uint16_t *end) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(*start) + sizeof(*end); + + if (pdu == NULL) + return 0; + + if (len < min_len) + return 0; + + if (start == NULL || end == NULL) + return 0; + + if (pdu[0] != ATT_OP_FIND_INFO_REQ) + return 0; + + *start = att_get_u16(&pdu[1]); + *end = att_get_u16(&pdu[3]); + + return min_len; +} + +uint16_t enc_find_info_resp(uint8_t format, struct att_data_list *list, + uint8_t *pdu, int len) +{ + uint8_t *ptr; + int i, w; + + if (pdu == NULL) + return 0; + + if (list == NULL) + return 0; + + if (len < list->len + 2) + return 0; + + pdu[0] = ATT_OP_FIND_INFO_RESP; + pdu[1] = format; + ptr = (void *) &pdu[2]; + + for (i = 0, w = 2; i < list->num && w + list->len <= len; i++) { + memcpy(ptr, list->data[i], list->len); + ptr += list->len; + w += list->len; + } + + return w; +} + +struct att_data_list *dec_find_info_resp(const uint8_t *pdu, int len, + uint8_t *format) +{ + struct att_data_list *list; + uint8_t *ptr; + uint16_t elen, num; + int i; + + if (pdu == NULL) + return 0; + + if (format == NULL) + return 0; + + if (pdu[0] != ATT_OP_FIND_INFO_RESP) + return 0; + + *format = pdu[1]; + elen = sizeof(pdu[0]) + sizeof(*format); + if (*format == 0x01) + elen += 2; + else if (*format == 0x02) + elen += 16; + + num = (len - 2) / elen; + + ptr = (void *) &pdu[2]; + + list = att_data_list_alloc(num, elen); + + for (i = 0; i < num; i++) { + memcpy(list->data[i], ptr, list->len); + ptr += list->len; + } + + return list; +} + +uint16_t enc_notification(uint16_t handle, uint8_t *value, int vlen, + uint8_t *pdu, int len) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(uint16_t); + + if (pdu == NULL) + return 0; + + if (len < (vlen + min_len)) + return 0; + + pdu[0] = ATT_OP_HANDLE_NOTIFY; + att_put_u16(handle, &pdu[1]); + memcpy(&pdu[3], value, vlen); + + return vlen + min_len; +} + +uint16_t enc_indication(uint16_t handle, uint8_t *value, int vlen, + uint8_t *pdu, int len) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(uint16_t); + + if (pdu == NULL) + return 0; + + if (len < (vlen + min_len)) + return 0; + + pdu[0] = ATT_OP_HANDLE_IND; + att_put_u16(handle, &pdu[1]); + memcpy(&pdu[3], value, vlen); + + return vlen + min_len; +} + +uint16_t dec_indication(const uint8_t *pdu, int len, uint16_t *handle, + uint8_t *value, int vlen) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(uint16_t); + uint16_t dlen; + + if (pdu == NULL) + return 0; + + if (pdu[0] != ATT_OP_HANDLE_IND) + return 0; + + if (len < min_len) + return 0; + + dlen = MIN(len - min_len, vlen); + + if (handle) + *handle = att_get_u16(&pdu[1]); + + memcpy(value, &pdu[3], dlen); + + return dlen; +} + +uint16_t enc_confirmation(uint8_t *pdu, int len) +{ + const uint16_t min_len = sizeof(pdu[0]); + + if (pdu == NULL) + return 0; + + if (len < min_len) + return 0; + + pdu[0] = ATT_OP_HANDLE_CNF; + + return min_len; +} + +uint16_t enc_mtu_req(uint16_t mtu, uint8_t *pdu, int len) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(mtu); + + if (pdu == NULL) + return 0; + + if (len < min_len) + return 0; + + pdu[0] = ATT_OP_MTU_REQ; + att_put_u16(mtu, &pdu[1]); + + return min_len; +} + +uint16_t dec_mtu_req(const uint8_t *pdu, int len, uint16_t *mtu) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(*mtu); + + if (pdu == NULL) + return 0; + + if (mtu == NULL) + return 0; + + if (len < min_len) + return 0; + + if (pdu[0] != ATT_OP_MTU_REQ) + return 0; + + *mtu = att_get_u16(&pdu[1]); + + return min_len; +} + +uint16_t enc_mtu_resp(uint16_t mtu, uint8_t *pdu, int len) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(mtu); + + if (pdu == NULL) + return 0; + + if (len < min_len) + return 0; + + pdu[0] = ATT_OP_MTU_RESP; + att_put_u16(mtu, &pdu[1]); + + return min_len; +} + +uint16_t dec_mtu_resp(const uint8_t *pdu, int len, uint16_t *mtu) +{ + const uint16_t min_len = sizeof(pdu[0]) + sizeof(*mtu); + + if (pdu == NULL) + return 0; + + if (mtu == NULL) + return 0; + + if (len < min_len) + return 0; + + if (pdu[0] != ATT_OP_MTU_RESP) + return 0; + + *mtu = att_get_u16(&pdu[1]); + + return min_len; +} diff --git a/attrib/att.h b/attrib/att.h new file mode 100644 index 0000000..144513f --- /dev/null +++ b/attrib/att.h @@ -0,0 +1,258 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2010 Nokia Corporation + * Copyright (C) 2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +/* Attribute Protocol Opcodes */ +#define ATT_OP_ERROR 0x01 +#define ATT_OP_MTU_REQ 0x02 +#define ATT_OP_MTU_RESP 0x03 +#define ATT_OP_FIND_INFO_REQ 0x04 +#define ATT_OP_FIND_INFO_RESP 0x05 +#define ATT_OP_FIND_BY_TYPE_REQ 0x06 +#define ATT_OP_FIND_BY_TYPE_RESP 0x07 +#define ATT_OP_READ_BY_TYPE_REQ 0x08 +#define ATT_OP_READ_BY_TYPE_RESP 0x09 +#define ATT_OP_READ_REQ 0x0A +#define ATT_OP_READ_RESP 0x0B +#define ATT_OP_READ_BLOB_REQ 0x0C +#define ATT_OP_READ_BLOB_RESP 0x0D +#define ATT_OP_READ_MULTI_REQ 0x0E +#define ATT_OP_READ_MULTI_RESP 0x0F +#define ATT_OP_READ_BY_GROUP_REQ 0x10 +#define ATT_OP_READ_BY_GROUP_RESP 0x11 +#define ATT_OP_WRITE_REQ 0x12 +#define ATT_OP_WRITE_RESP 0x13 +#define ATT_OP_WRITE_CMD 0x52 +#define ATT_OP_PREP_WRITE_REQ 0x16 +#define ATT_OP_PREP_WRITE_RESP 0x17 +#define ATT_OP_EXEC_WRITE_REQ 0x18 +#define ATT_OP_EXEC_WRITE_RESP 0x19 +#define ATT_OP_HANDLE_NOTIFY 0x1B +#define ATT_OP_HANDLE_IND 0x1D +#define ATT_OP_HANDLE_CNF 0x1E +#define ATT_OP_SIGNED_WRITE_CMD 0xD2 + +/* Error codes for Error response PDU */ +#define ATT_ECODE_INVALID_HANDLE 0x01 +#define ATT_ECODE_READ_NOT_PERM 0x02 +#define ATT_ECODE_WRITE_NOT_PERM 0x03 +#define ATT_ECODE_INVALID_PDU 0x04 +#define ATT_ECODE_AUTHENTICATION 0x05 +#define ATT_ECODE_REQ_NOT_SUPP 0x06 +#define ATT_ECODE_INVALID_OFFSET 0x07 +#define ATT_ECODE_AUTHORIZATION 0x08 +#define ATT_ECODE_PREP_QUEUE_FULL 0x09 +#define ATT_ECODE_ATTR_NOT_FOUND 0x0A +#define ATT_ECODE_ATTR_NOT_LONG 0x0B +#define ATT_ECODE_INSUFF_ENCR_KEY_SIZE 0x0C +#define ATT_ECODE_INVAL_ATTR_VALUE_LEN 0x0D +#define ATT_ECODE_UNLIKELY 0x0E +#define ATT_ECODE_INSUFF_ENC 0x0F +#define ATT_ECODE_UNSUPP_GRP_TYPE 0x10 +#define ATT_ECODE_INSUFF_RESOURCES 0x11 +/* Application error */ +#define ATT_ECODE_IO 0x80 +#define ATT_ECODE_TIMEOUT 0x81 +#define ATT_ECODE_ABORTED 0x82 + +/* Characteristic Property bit field */ +#define ATT_CHAR_PROPER_BROADCAST 0x01 +#define ATT_CHAR_PROPER_READ 0x02 +#define ATT_CHAR_PROPER_WRITE_WITHOUT_RESP 0x04 +#define ATT_CHAR_PROPER_WRITE 0x08 +#define ATT_CHAR_PROPER_NOTIFY 0x10 +#define ATT_CHAR_PROPER_INDICATE 0x20 +#define ATT_CHAR_PROPER_AUTH 0x40 +#define ATT_CHAR_PROPER_EXT_PROPER 0x80 + +#define ATT_MAX_MTU 256 +#define ATT_DEFAULT_L2CAP_MTU 48 +#define ATT_DEFAULT_LE_MTU 23 + +#define ATT_CID 4 +#define ATT_PSM 31 + +struct att_data_list { + uint16_t num; + uint16_t len; + uint8_t **data; +}; + +struct att_range { + uint16_t start; + uint16_t end; +}; + +/* These functions do byte conversion */ +static inline uint8_t att_get_u8(const void *ptr) +{ + const uint8_t *u8_ptr = ptr; + return bt_get_unaligned(u8_ptr); +} + +static inline uint16_t att_get_u16(const void *ptr) +{ + const uint16_t *u16_ptr = ptr; + return btohs(bt_get_unaligned(u16_ptr)); +} + +static inline uint32_t att_get_u32(const void *ptr) +{ + const uint32_t *u32_ptr = ptr; + return btohl(bt_get_unaligned(u32_ptr)); +} + +static inline uint128_t att_get_u128(const void *ptr) +{ + const uint128_t *u128_ptr = ptr; + uint128_t dst; + + btoh128(u128_ptr, &dst); + + return dst; +} + +static inline void att_put_u8(uint8_t src, void *dst) +{ + bt_put_unaligned(src, (uint8_t *) dst); +} + +static inline void att_put_u16(uint16_t src, void *dst) +{ + bt_put_unaligned(htobs(src), (uint16_t *) dst); +} + +static inline void att_put_u32(uint32_t src, void *dst) +{ + bt_put_unaligned(htobl(src), (uint32_t *) dst); +} + +static inline void att_put_u128(uint128_t src, void *dst) +{ + uint128_t *d128 = dst; + + htob128(&src, d128); +} + +static inline void att_put_uuid16(bt_uuid_t src, void *dst) +{ + att_put_u16(src.value.u16, dst); +} + +static inline void att_put_uuid128(bt_uuid_t src, void *dst) +{ + att_put_u128(src.value.u128, dst); +} + +static inline void att_put_uuid(bt_uuid_t src, void *dst) +{ + if (src.type == BT_UUID16) + att_put_uuid16(src, dst); + else + att_put_uuid128(src, dst); +} + +static inline bt_uuid_t att_get_uuid16(const void *ptr) +{ + bt_uuid_t uuid; + + bt_uuid16_create(&uuid, att_get_u16(ptr)); + + return uuid; +} + +static inline bt_uuid_t att_get_uuid128(const void *ptr) +{ + bt_uuid_t uuid; + uint128_t value; + + value = att_get_u128(ptr); + bt_uuid128_create(&uuid, value); + + return uuid; +} + +struct att_data_list *att_data_list_alloc(uint16_t num, uint16_t len); +void att_data_list_free(struct att_data_list *list); + +const char *att_ecode2str(uint8_t status); +uint16_t enc_read_by_grp_req(uint16_t start, uint16_t end, bt_uuid_t *uuid, + uint8_t *pdu, int len); +uint16_t dec_read_by_grp_req(const uint8_t *pdu, int len, uint16_t *start, + uint16_t *end, bt_uuid_t *uuid); +uint16_t enc_read_by_grp_resp(struct att_data_list *list, uint8_t *pdu, int len); +uint16_t enc_find_by_type_req(uint16_t start, uint16_t end, bt_uuid_t *uuid, + const uint8_t *value, int vlen, uint8_t *pdu, int len); +uint16_t dec_find_by_type_req(const uint8_t *pdu, int len, uint16_t *start, + uint16_t *end, bt_uuid_t *uuid, uint8_t *value, int *vlen); +uint16_t enc_find_by_type_resp(GSList *ranges, uint8_t *pdu, int len); +GSList *dec_find_by_type_resp(const uint8_t *pdu, int len); +struct att_data_list *dec_read_by_grp_resp(const uint8_t *pdu, int len); +uint16_t enc_read_by_type_req(uint16_t start, uint16_t end, bt_uuid_t *uuid, + uint8_t *pdu, int len); +uint16_t dec_read_by_type_req(const uint8_t *pdu, int len, uint16_t *start, + uint16_t *end, bt_uuid_t *uuid); +uint16_t enc_read_by_type_resp(struct att_data_list *list, uint8_t *pdu, + int len); +uint16_t enc_write_cmd(uint16_t handle, const uint8_t *value, int vlen, + uint8_t *pdu, int len); +uint16_t dec_write_cmd(const uint8_t *pdu, int len, uint16_t *handle, + uint8_t *value, int *vlen); +struct att_data_list *dec_read_by_type_resp(const uint8_t *pdu, int len); +uint16_t enc_write_req(uint16_t handle, const uint8_t *value, int vlen, + uint8_t *pdu, int len); +uint16_t dec_write_req(const uint8_t *pdu, int len, uint16_t *handle, + uint8_t *value, int *vlen); +uint16_t enc_write_resp(uint8_t *pdu, int len); +uint16_t dec_write_resp(const uint8_t *pdu, int len); +uint16_t enc_read_req(uint16_t handle, uint8_t *pdu, int len); +uint16_t enc_read_blob_req(uint16_t handle, uint16_t offset, uint8_t *pdu, + int len); +uint16_t dec_read_req(const uint8_t *pdu, int len, uint16_t *handle); +uint16_t dec_read_blob_req(const uint8_t *pdu, int len, uint16_t *handle, + uint16_t *offset); +uint16_t enc_read_resp(uint8_t *value, int vlen, uint8_t *pdu, int len); +uint16_t enc_read_blob_resp(uint8_t *value, int vlen, uint16_t offset, + uint8_t *pdu, int len); +uint16_t dec_read_resp(const uint8_t *pdu, int len, uint8_t *value, int *vlen); +uint16_t enc_error_resp(uint8_t opcode, uint16_t handle, uint8_t status, + uint8_t *pdu, int len); +uint16_t enc_find_info_req(uint16_t start, uint16_t end, uint8_t *pdu, int len); +uint16_t dec_find_info_req(const uint8_t *pdu, int len, uint16_t *start, + uint16_t *end); +uint16_t enc_find_info_resp(uint8_t format, struct att_data_list *list, + uint8_t *pdu, int len); +struct att_data_list *dec_find_info_resp(const uint8_t *pdu, int len, + uint8_t *format); +uint16_t enc_notification(uint16_t handle, uint8_t *value, int vlen, + uint8_t *pdu, int len); +uint16_t enc_indication(uint16_t handle, uint8_t *value, int vlen, + uint8_t *pdu, int len); +uint16_t dec_indication(const uint8_t *pdu, int len, uint16_t *handle, + uint8_t *value, int vlen); +uint16_t enc_confirmation(uint8_t *pdu, int len); + +uint16_t enc_mtu_req(uint16_t mtu, uint8_t *pdu, int len); +uint16_t dec_mtu_req(const uint8_t *pdu, int len, uint16_t *mtu); +uint16_t enc_mtu_resp(uint16_t mtu, uint8_t *pdu, int len); +uint16_t dec_mtu_resp(const uint8_t *pdu, int len, uint16_t *mtu); diff --git a/attrib/client.c b/attrib/client.c new file mode 100644 index 0000000..0b0ff1f --- /dev/null +++ b/attrib/client.c @@ -0,0 +1,1148 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2010 Nokia Corporation + * Copyright (C) 2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include + +#include +#include + +#include "adapter.h" +#include "device.h" +#include "log.h" +#include "gdbus.h" +#include "error.h" +#include "dbus-common.h" +#include "btio.h" +#include "storage.h" + +#include "att.h" +#include "gattrib.h" +#include "attio.h" +#include "gatt.h" +#include "client.h" + +#define CHAR_INTERFACE "org.bluez.Characteristic" + +struct format { + guint8 format; + guint8 exponent; + guint16 unit; + guint8 namespace; + guint16 desc; +} __attribute__ ((packed)); + +struct query { + DBusMessage *msg; + GSList *list; +}; + +struct gatt_service { + struct btd_device *dev; + struct gatt_primary *prim; + DBusConnection *conn; + GAttrib *attrib; + guint attioid; + int psm; + char *path; + GSList *chars; + GSList *offline_chars; + GSList *watchers; + struct query *query; +}; + +struct characteristic { + struct gatt_service *gatt; + char *path; + uint16_t handle; + uint16_t end; + uint8_t perm; + char type[MAX_LEN_UUID_STR + 1]; + char *name; + char *desc; + struct format *format; + uint8_t *value; + size_t vlen; +}; + +struct query_data { + struct gatt_service *gatt; + struct characteristic *chr; + uint16_t handle; +}; + +struct watcher { + guint id; + char *name; + char *path; + struct gatt_service *gatt; +}; + +static GSList *gatt_services = NULL; + +static void characteristic_free(void *user_data) +{ + struct characteristic *chr = user_data; + + g_free(chr->path); + g_free(chr->desc); + g_free(chr->format); + g_free(chr->value); + g_free(chr->name); + g_free(chr); +} + +static void watcher_free(void *user_data) +{ + struct watcher *watcher = user_data; + + g_free(watcher->path); + g_free(watcher->name); + g_free(watcher); +} + +static void gatt_service_free(struct gatt_service *gatt) +{ + g_slist_free_full(gatt->watchers, watcher_free); + g_slist_free_full(gatt->chars, characteristic_free); + g_slist_free(gatt->offline_chars); + g_free(gatt->path); + btd_device_unref(gatt->dev); + dbus_connection_unref(gatt->conn); + g_free(gatt); +} + +static void remove_attio(struct gatt_service *gatt) +{ + if (gatt->offline_chars || gatt->watchers || gatt->query) + return; + + if (gatt->attioid) { + btd_device_remove_attio_callback(gatt->dev, gatt->attioid); + gatt->attioid = 0; + } + + if (gatt->attrib) { + g_attrib_unref(gatt->attrib); + gatt->attrib = NULL; + } +} + +static void gatt_get_address(struct gatt_service *gatt, bdaddr_t *sba, + bdaddr_t *dba, uint8_t *bdaddr_type) +{ + struct btd_device *device = gatt->dev; + struct btd_adapter *adapter; + + adapter = device_get_adapter(device); + adapter_get_address(adapter, sba); + device_get_address(device, dba, bdaddr_type); +} + +static int characteristic_handle_cmp(gconstpointer a, gconstpointer b) +{ + const struct characteristic *chr = a; + uint16_t handle = GPOINTER_TO_UINT(b); + + return chr->handle - handle; +} + +static int watcher_cmp(gconstpointer a, gconstpointer b) +{ + const struct watcher *watcher = a; + const struct watcher *match = b; + int ret; + + ret = g_strcmp0(watcher->name, match->name); + if (ret != 0) + return ret; + + return g_strcmp0(watcher->path, match->path); +} + +static void append_char_dict(DBusMessageIter *iter, struct characteristic *chr) +{ + DBusMessageIter dict; + const char *name = ""; + char *uuid; + + dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + uuid = g_strdup(chr->type); + dict_append_entry(&dict, "UUID", DBUS_TYPE_STRING, &uuid); + g_free(uuid); + + /* FIXME: Translate UUID to name. */ + dict_append_entry(&dict, "Name", DBUS_TYPE_STRING, &name); + + if (chr->desc) + dict_append_entry(&dict, "Description", DBUS_TYPE_STRING, + &chr->desc); + + if (chr->value) + dict_append_array(&dict, "Value", DBUS_TYPE_BYTE, &chr->value, + chr->vlen); + + /* FIXME: Missing Format, Value and Representation */ + + dbus_message_iter_close_container(iter, &dict); +} + +static void watcher_exit(DBusConnection *conn, void *user_data) +{ + struct watcher *watcher = user_data; + struct gatt_service *gatt = watcher->gatt; + + DBG("%s watcher %s exited", gatt->path, watcher->name); + + gatt->watchers = g_slist_remove(gatt->watchers, watcher); + g_dbus_remove_watch(gatt->conn, watcher->id); + remove_attio(gatt); +} + +static int characteristic_set_value(struct characteristic *chr, + const uint8_t *value, size_t vlen) +{ + chr->value = g_try_realloc(chr->value, vlen); + if (chr->value == NULL) + return -ENOMEM; + + memcpy(chr->value, value, vlen); + chr->vlen = vlen; + + return 0; +} + +static void update_watchers(gpointer data, gpointer user_data) +{ + struct watcher *w = data; + struct characteristic *chr = user_data; + DBusConnection *conn = w->gatt->conn; + DBusMessage *msg; + + msg = dbus_message_new_method_call(w->name, w->path, + "org.bluez.Watcher", "ValueChanged"); + if (msg == NULL) + return; + + dbus_message_append_args(msg, DBUS_TYPE_OBJECT_PATH, &chr->path, + DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, + &chr->value, chr->vlen, DBUS_TYPE_INVALID); + + dbus_message_set_no_reply(msg, TRUE); + g_dbus_send_message(conn, msg); +} + +static void events_handler(const uint8_t *pdu, uint16_t len, + gpointer user_data) +{ + struct gatt_service *gatt = user_data; + struct characteristic *chr; + GSList *l; + uint8_t opdu[ATT_MAX_MTU]; + guint handle; + uint16_t olen; + + if (len < 3) { + DBG("Malformed notification/indication packet (opcode 0x%02x)", + pdu[0]); + return; + } + + handle = att_get_u16(&pdu[1]); + + l = g_slist_find_custom(gatt->chars, GUINT_TO_POINTER(handle), + characteristic_handle_cmp); + if (!l) + return; + + chr = l->data; + + if (chr == NULL) { + DBG("Attribute handle 0x%02x not found", handle); + return; + } + + switch (pdu[0]) { + case ATT_OP_HANDLE_IND: + olen = enc_confirmation(opdu, sizeof(opdu)); + g_attrib_send(gatt->attrib, 0, opdu[0], opdu, olen, + NULL, NULL, NULL); + case ATT_OP_HANDLE_NOTIFY: + if (characteristic_set_value(chr, &pdu[3], len - 3) < 0) + DBG("Can't change Characteristic 0x%02x", handle); + + g_slist_foreach(gatt->watchers, update_watchers, chr); + break; + } +} + +static void offline_char_written(gpointer user_data) +{ + struct characteristic *chr = user_data; + struct gatt_service *gatt = chr->gatt; + + gatt->offline_chars = g_slist_remove(gatt->offline_chars, chr); + + remove_attio(gatt); +} + +static void offline_char_write(gpointer data, gpointer user_data) +{ + struct characteristic *chr = data; + GAttrib *attrib = user_data; + + gatt_write_cmd(attrib, chr->handle, chr->value, chr->vlen, + offline_char_written, chr); +} + +static void char_discovered_cb(GSList *characteristics, guint8 status, + gpointer user_data); + +static void attio_connected(GAttrib *attrib, gpointer user_data) +{ + struct gatt_service *gatt = user_data; + + gatt->attrib = g_attrib_ref(attrib); + + g_attrib_register(gatt->attrib, ATT_OP_HANDLE_NOTIFY, + events_handler, gatt, NULL); + g_attrib_register(gatt->attrib, ATT_OP_HANDLE_IND, + events_handler, gatt, NULL); + + g_slist_foreach(gatt->offline_chars, offline_char_write, attrib); + + if (gatt->query) { + struct gatt_primary *prim = gatt->prim; + struct query_data *qchr; + + qchr = g_slist_nth_data(gatt->query->list, 0); + gatt_discover_char(gatt->attrib, prim->range.start, + prim->range.end, NULL, + char_discovered_cb, qchr); + } +} + +static void attio_disconnected(gpointer user_data) +{ + struct gatt_service *gatt = user_data; + + if (gatt->query && gatt->query->msg) { + DBusMessage *reply; + + reply = btd_error_failed(gatt->query->msg, + "ATT IO channel was disconnected"); + g_dbus_send_message(gatt->conn, reply); + dbus_message_unref(gatt->query->msg); + } + + if (gatt->query) { + g_slist_free_full(gatt->query->list, g_free); + gatt->query = NULL; + } + + if (gatt->attrib) { + g_attrib_unref(gatt->attrib); + gatt->attrib = NULL; + } +} + +static DBusMessage *register_watcher(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + const char *sender = dbus_message_get_sender(msg); + struct gatt_service *gatt = data; + struct watcher *watcher; + char *path; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + watcher = g_new0(struct watcher, 1); + watcher->name = g_strdup(sender); + watcher->gatt = gatt; + watcher->path = g_strdup(path); + watcher->id = g_dbus_add_disconnect_watch(conn, sender, watcher_exit, + watcher, watcher_free); + + if (gatt->attioid == 0) + gatt->attioid = btd_device_add_attio_callback(gatt->dev, + attio_connected, + attio_disconnected, + gatt); + + gatt->watchers = g_slist_append(gatt->watchers, watcher); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *unregister_watcher(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + const char *sender = dbus_message_get_sender(msg); + struct gatt_service *gatt = data; + struct watcher *watcher, *match; + GSList *l; + char *path; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + match = g_new0(struct watcher, 1); + match->name = g_strdup(sender); + match->path = g_strdup(path); + l = g_slist_find_custom(gatt->watchers, match, watcher_cmp); + watcher_free(match); + if (!l) + return btd_error_not_authorized(msg); + + watcher = l->data; + gatt->watchers = g_slist_remove(gatt->watchers, watcher); + g_dbus_remove_watch(conn, watcher->id); + remove_attio(gatt); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *set_value(DBusConnection *conn, DBusMessage *msg, + DBusMessageIter *iter, struct characteristic *chr) +{ + struct gatt_service *gatt = chr->gatt; + DBusMessageIter sub; + uint8_t *value; + int len; + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY || + dbus_message_iter_get_element_type(iter) != DBUS_TYPE_BYTE) + return btd_error_invalid_args(msg); + + dbus_message_iter_recurse(iter, &sub); + + dbus_message_iter_get_fixed_array(&sub, &value, &len); + + characteristic_set_value(chr, value, len); + + if (gatt->attioid == 0) + gatt->attioid = btd_device_add_attio_callback(gatt->dev, + attio_connected, + attio_disconnected, + gatt); + + if (gatt->attrib) + gatt_write_cmd(gatt->attrib, chr->handle, value, len, + NULL, NULL); + else + gatt->offline_chars = g_slist_append(gatt->offline_chars, chr); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *get_properties(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct characteristic *chr = data; + DBusMessage *reply; + DBusMessageIter iter; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + append_char_dict(&iter, chr); + + return reply; +} + +static DBusMessage *set_property(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct characteristic *chr = data; + DBusMessageIter iter; + DBusMessageIter sub; + const char *property; + + if (!dbus_message_iter_init(msg, &iter)) + return btd_error_invalid_args(msg); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&iter, &property); + dbus_message_iter_next(&iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) + return btd_error_invalid_args(msg); + + dbus_message_iter_recurse(&iter, &sub); + + if (g_str_equal("Value", property)) + return set_value(conn, msg, &sub, chr); + + return btd_error_invalid_args(msg); +} + +static const GDBusMethodTable char_methods[] = { + { GDBUS_METHOD("GetProperties", + NULL, GDBUS_ARGS({ "properties", "a{sv}" }), + get_properties) }, + { GDBUS_METHOD("SetProperty", + GDBUS_ARGS({ "name", "s" }, { "value", "v" }), NULL, + set_property) }, + { } +}; + +static char *characteristic_list_to_string(GSList *chars) +{ + GString *characteristics; + GSList *l; + + characteristics = g_string_new(NULL); + + for (l = chars; l; l = l->next) { + struct characteristic *chr = l->data; + char chr_str[64]; + + memset(chr_str, 0, sizeof(chr_str)); + + snprintf(chr_str, sizeof(chr_str), "%04X#%02X#%04X#%s ", + chr->handle, chr->perm, chr->end, chr->type); + + characteristics = g_string_append(characteristics, chr_str); + } + + return g_string_free(characteristics, FALSE); +} + +static void store_characteristics(const bdaddr_t *sba, const bdaddr_t *dba, + uint8_t bdaddr_type, uint16_t start, + GSList *chars) +{ + char *characteristics; + + characteristics = characteristic_list_to_string(chars); + + write_device_characteristics(sba, dba, bdaddr_type, start, + characteristics); + + g_free(characteristics); +} + +static void register_characteristic(gpointer data, gpointer user_data) +{ + struct characteristic *chr = data; + DBusConnection *conn = chr->gatt->conn; + const char *gatt_path = user_data; + + chr->path = g_strdup_printf("%s/characteristic%04x", gatt_path, + chr->handle); + + g_dbus_register_interface(conn, chr->path, CHAR_INTERFACE, + char_methods, NULL, NULL, chr, NULL); + + DBG("Registered: %s", chr->path); +} + +static GSList *string_to_characteristic_list(struct gatt_service *gatt, + const char *str) +{ + GSList *l = NULL; + char **chars; + int i; + + if (str == NULL) + return NULL; + + chars = g_strsplit(str, " ", 0); + if (chars == NULL) + return NULL; + + for (i = 0; chars[i]; i++) { + struct characteristic *chr; + int ret; + + chr = g_new0(struct characteristic, 1); + + ret = sscanf(chars[i], "%04hX#%02hhX#%04hX#%s", &chr->handle, + &chr->perm, &chr->end, chr->type); + if (ret < 4) { + g_free(chr); + continue; + } + + chr->gatt = gatt; + l = g_slist_append(l, chr); + } + + g_strfreev(chars); + + return l; +} + +static GSList *load_characteristics(struct gatt_service *gatt, uint16_t start) +{ + GSList *chrs_list; + bdaddr_t sba, dba; + uint8_t bdaddr_type; + char *str; + + gatt_get_address(gatt, &sba, &dba, &bdaddr_type); + + str = read_device_characteristics(&sba, &dba, bdaddr_type, start); + if (str == NULL) + return NULL; + + chrs_list = string_to_characteristic_list(gatt, str); + + free(str); + + return chrs_list; +} + +static void store_attribute(struct gatt_service *gatt, uint16_t handle, + uint16_t type, uint8_t *value, gsize len) +{ + struct btd_device *device = gatt->dev; + bdaddr_t sba, dba; + uint8_t bdaddr_type; + bt_uuid_t uuid; + char *str, *tmp; + guint i; + + str = g_malloc0(MAX_LEN_UUID_STR + len * 2 + 1); + + bt_uuid16_create(&uuid, type); + bt_uuid_to_string(&uuid, str, MAX_LEN_UUID_STR); + + str[MAX_LEN_UUID_STR - 1] = '#'; + + for (i = 0, tmp = str + MAX_LEN_UUID_STR; i < len; i++, tmp += 2) + sprintf(tmp, "%02X", value[i]); + + gatt_get_address(gatt, &sba, &dba, NULL); + + bdaddr_type = device_get_addr_type(device); + + write_device_attribute(&sba, &dba, bdaddr_type, handle, str); + + g_free(str); +} + +static void query_list_append(struct gatt_service *gatt, struct query_data *data) +{ + struct query *query = gatt->query; + + query->list = g_slist_append(query->list, data); +} + +static void query_list_remove(struct gatt_service *gatt, struct query_data *data) +{ + struct query *query = gatt->query; + + query->list = g_slist_remove(query->list, data); + if (query->list != NULL) + return; + + g_free(query); + gatt->query = NULL; + + remove_attio(gatt); +} + +static void update_char_desc(guint8 status, const guint8 *pdu, guint16 len, + gpointer user_data) +{ + struct query_data *current = user_data; + struct gatt_service *gatt = current->gatt; + struct characteristic *chr = current->chr; + + if (status == 0) { + + g_free(chr->desc); + + chr->desc = g_malloc(len); + memcpy(chr->desc, pdu + 1, len - 1); + chr->desc[len - 1] = '\0'; + + store_attribute(gatt, current->handle, + GATT_CHARAC_USER_DESC_UUID, + (void *) chr->desc, len); + } else if (status == ATT_ECODE_INSUFF_ENC) { + GIOChannel *io = g_attrib_get_channel(gatt->attrib); + BtIOSecLevel level = BT_IO_SEC_HIGH; + + bt_io_get(io, BT_IO_L2CAP, NULL, + BT_IO_OPT_SEC_LEVEL, &level, + BT_IO_OPT_INVALID); + + if (level < BT_IO_SEC_HIGH) + level++; + + if (bt_io_set(io, BT_IO_L2CAP, NULL, + BT_IO_OPT_SEC_LEVEL, level, + BT_IO_OPT_INVALID)) { + gatt_read_char(gatt->attrib, current->handle, 0, + update_char_desc, current); + return; + } + } + + query_list_remove(gatt, current); + g_free(current); +} + +static void update_char_format(guint8 status, const guint8 *pdu, guint16 len, + gpointer user_data) +{ + struct query_data *current = user_data; + struct gatt_service *gatt = current->gatt; + struct characteristic *chr = current->chr; + + if (status != 0) + goto done; + + if (len < 8) + goto done; + + g_free(chr->format); + + chr->format = g_new0(struct format, 1); + memcpy(chr->format, pdu + 1, 7); + + store_attribute(gatt, current->handle, GATT_CHARAC_FMT_UUID, + (void *) chr->format, sizeof(*chr->format)); + +done: + query_list_remove(gatt, current); + g_free(current); +} + +static void update_char_value(guint8 status, const guint8 *pdu, + guint16 len, gpointer user_data) +{ + struct query_data *current = user_data; + struct gatt_service *gatt = current->gatt; + struct characteristic *chr = current->chr; + + if (status == 0) + characteristic_set_value(chr, pdu + 1, len - 1); + else if (status == ATT_ECODE_INSUFF_ENC) { + GIOChannel *io = g_attrib_get_channel(gatt->attrib); + + if (bt_io_set(io, BT_IO_L2CAP, NULL, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_HIGH, + BT_IO_OPT_INVALID)) { + gatt_read_char(gatt->attrib, chr->handle, 0, + update_char_value, current); + return; + } + } + + query_list_remove(gatt, current); + g_free(current); +} + +static int uuid_desc16_cmp(bt_uuid_t *uuid, guint16 desc) +{ + bt_uuid_t u16; + + bt_uuid16_create(&u16, desc); + + return bt_uuid_cmp(uuid, &u16); +} + +static void descriptor_cb(guint8 status, const guint8 *pdu, guint16 plen, + gpointer user_data) +{ + struct query_data *current = user_data; + struct gatt_service *gatt = current->gatt; + struct att_data_list *list; + guint8 format; + int i; + + if (status != 0) + goto done; + + DBG("Find Information Response received"); + + list = dec_find_info_resp(pdu, plen, &format); + if (list == NULL) + goto done; + + for (i = 0; i < list->num; i++) { + guint16 handle; + bt_uuid_t uuid; + uint8_t *info = list->data[i]; + struct query_data *qfmt; + + handle = att_get_u16(info); + + if (format == 0x01) { + uuid = att_get_uuid16(&info[2]); + } else { + /* Currently, only "user description" and "presentation + * format" descriptors are used, and both have 16-bit + * UUIDs. Therefore there is no need to support format + * 0x02 yet. */ + continue; + } + qfmt = g_new0(struct query_data, 1); + qfmt->gatt = current->gatt; + qfmt->chr = current->chr; + qfmt->handle = handle; + + if (uuid_desc16_cmp(&uuid, GATT_CHARAC_USER_DESC_UUID) == 0) { + query_list_append(gatt, qfmt); + gatt_read_char(gatt->attrib, handle, 0, update_char_desc, + qfmt); + } else if (uuid_desc16_cmp(&uuid, GATT_CHARAC_FMT_UUID) == 0) { + query_list_append(gatt, qfmt); + gatt_read_char(gatt->attrib, handle, 0, + update_char_format, qfmt); + } else + g_free(qfmt); + } + + att_data_list_free(list); +done: + query_list_remove(gatt, current); + g_free(current); +} + +static void update_all_chars(gpointer data, gpointer user_data) +{ + struct query_data *qdesc, *qvalue; + struct characteristic *chr = data; + struct gatt_service *gatt = user_data; + + qdesc = g_new0(struct query_data, 1); + qdesc->gatt = gatt; + qdesc->chr = chr; + + query_list_append(gatt, qdesc); + + gatt_find_info(gatt->attrib, chr->handle + 1, chr->end, descriptor_cb, + qdesc); + + qvalue = g_new0(struct query_data, 1); + qvalue->gatt = gatt; + qvalue->chr = chr; + + query_list_append(gatt, qvalue); + + gatt_read_char(gatt->attrib, chr->handle, 0, update_char_value, qvalue); +} + +static DBusMessage *create_discover_char_reply(DBusMessage *msg, GSList *chars) +{ + DBusMessage *reply; + DBusMessageIter iter, array_iter; + GSList *l; + + reply = dbus_message_new_method_return(msg); + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_TYPE_OBJECT_PATH_AS_STRING, &array_iter); + + for (l = chars; l; l = l->next) { + struct characteristic *chr = l->data; + + dbus_message_iter_append_basic(&array_iter, + DBUS_TYPE_OBJECT_PATH, &chr->path); + } + + dbus_message_iter_close_container(&iter, &array_iter); + + return reply; +} + +static void char_discovered_cb(GSList *characteristics, guint8 status, + gpointer user_data) +{ + DBusMessage *reply; + struct query_data *current = user_data; + struct gatt_service *gatt = current->gatt; + struct gatt_primary *prim = gatt->prim; + uint16_t *previous_end = NULL; + GSList *l; + bdaddr_t sba, dba; + uint8_t bdaddr_type; + + if (status != 0) { + const char *str = att_ecode2str(status); + + DBG("Discover all characteristics failed: %s", str); + reply = btd_error_failed(gatt->query->msg, str); + goto fail; + } + + for (l = characteristics; l; l = l->next) { + struct gatt_char *current_chr = l->data; + struct characteristic *chr; + guint handle = current_chr->value_handle; + GSList *lchr; + + lchr = g_slist_find_custom(gatt->chars, + GUINT_TO_POINTER(handle), characteristic_handle_cmp); + if (lchr) + continue; + + chr = g_new0(struct characteristic, 1); + chr->gatt = gatt; + chr->perm = current_chr->properties; + chr->handle = current_chr->value_handle; +#ifdef __TIZEN_PATCH__ + strncpy(chr->type, current_chr->uuid, sizeof(chr->type)); +#else + strncpy(chr->type, current_chr->uuid, sizeof(chr->type) - 1); +#endif + + if (previous_end) + *previous_end = current_chr->handle; + + previous_end = &chr->end; + + gatt->chars = g_slist_append(gatt->chars, chr); + register_characteristic(chr, gatt->path); + } + + if (previous_end) + *previous_end = prim->range.end; + + gatt_get_address(gatt, &sba, &dba, &bdaddr_type); + store_characteristics(&sba, &dba, bdaddr_type, prim->range.start, + gatt->chars); + + g_slist_foreach(gatt->chars, update_all_chars, gatt); + + reply = create_discover_char_reply(gatt->query->msg, gatt->chars); + +fail: + dbus_message_unref(gatt->query->msg); + gatt->query->msg = NULL; + + g_dbus_send_message(gatt->conn, reply); + query_list_remove(gatt, current); + g_free(current); +} + +static DBusMessage *discover_char(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct gatt_service *gatt = data; + struct query *query; + struct query_data *qchr; + + if (gatt->query) + return btd_error_busy(msg); + + query = g_new0(struct query, 1); + + qchr = g_new0(struct query_data, 1); + qchr->gatt = gatt; + + query->msg = dbus_message_ref(msg); + + if (gatt->attioid == 0) { + gatt->attioid = btd_device_add_attio_callback(gatt->dev, + attio_connected, + attio_disconnected, + gatt); + } else if (gatt->attrib) { + struct gatt_primary *prim = gatt->prim; + gatt_discover_char(gatt->attrib, prim->range.start, + prim->range.end, NULL, + char_discovered_cb, qchr); + } + + gatt->query = query; + + query_list_append(gatt, qchr); + + return NULL; +} + +static DBusMessage *prim_get_properties(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct gatt_service *gatt = data; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + GSList *l; + char **chars; + const char *uuid; + int i; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + chars = g_new0(char *, g_slist_length(gatt->chars) + 1); + + for (i = 0, l = gatt->chars; l; l = l->next, i++) { + struct characteristic *chr = l->data; + chars[i] = chr->path; + } + + dict_append_array(&dict, "Characteristics", DBUS_TYPE_OBJECT_PATH, + &chars, i); + uuid = gatt->prim->uuid; + dict_append_entry(&dict, "UUID", DBUS_TYPE_STRING, &uuid); + + g_free(chars); + + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static const GDBusMethodTable prim_methods[] = { + { GDBUS_ASYNC_METHOD("DiscoverCharacteristics", + NULL, GDBUS_ARGS({ "characteristics", "ao" }), + discover_char) }, + { GDBUS_METHOD("RegisterCharacteristicsWatcher", + GDBUS_ARGS({ "agent", "o" }), NULL, + register_watcher) }, + { GDBUS_METHOD("UnregisterCharacteristicsWatcher", + GDBUS_ARGS({ "agent", "o" }), NULL, + unregister_watcher) }, + { GDBUS_METHOD("GetProperties", + NULL, GDBUS_ARGS({ "properties", "a{sv}" }), + prim_get_properties) }, + { } +}; + +static struct gatt_service *primary_register(DBusConnection *conn, + struct btd_device *device, + struct gatt_primary *prim, + int psm) +{ + struct gatt_service *gatt; + const char *device_path; + + device_path = device_get_path(device); + + gatt = g_new0(struct gatt_service, 1); + gatt->dev = btd_device_ref(device); + gatt->prim = prim; + gatt->psm = psm; + gatt->conn = dbus_connection_ref(conn); + gatt->path = g_strdup_printf("%s/service%04x", device_path, + prim->range.start); + + g_dbus_register_interface(gatt->conn, gatt->path, + CHAR_INTERFACE, prim_methods, + NULL, NULL, gatt, NULL); + gatt->chars = load_characteristics(gatt, prim->range.start); + g_slist_foreach(gatt->chars, register_characteristic, gatt->path); + + return gatt; +} + +GSList *attrib_client_register(DBusConnection *connection, + struct btd_device *device, int psm, + GAttrib *attrib, GSList *primaries) +{ + GSList *l, *services; + + for (l = primaries, services = NULL; l; l = l->next) { + struct gatt_primary *prim = l->data; + struct gatt_service *gatt; + + gatt = primary_register(connection, device, prim, psm); + + DBG("Registered: %s", gatt->path); + + services = g_slist_append(services, g_strdup(gatt->path)); + gatt_services = g_slist_append(gatt_services, gatt); + + } + + return services; +} + +static void primary_unregister(struct gatt_service *gatt) +{ + GSList *l; + + for (l = gatt->chars; l; l = l->next) { + struct characteristic *chr = l->data; + g_dbus_unregister_interface(gatt->conn, chr->path, + CHAR_INTERFACE); + } + + g_dbus_unregister_interface(gatt->conn, gatt->path, CHAR_INTERFACE); + + remove_attio(gatt); +} + +static int path_cmp(gconstpointer data, gconstpointer user_data) +{ + const char *path = data; + const char *gatt_path = user_data; + + return g_strcmp0(path, gatt_path); +} + +void attrib_client_unregister(GSList *services) +{ + GSList *l, *left; + + for (l = gatt_services, left = NULL; l; l = l->next) { + struct gatt_service *gatt = l->data; + + if (!g_slist_find_custom(services, gatt->path, path_cmp)) { + left = g_slist_append(left, gatt); + continue; + } + + primary_unregister(gatt); + gatt_service_free(gatt); + } + + g_slist_free(gatt_services); + gatt_services = left; +} diff --git a/attrib/client.h b/attrib/client.h new file mode 100644 index 0000000..948f030 --- /dev/null +++ b/attrib/client.h @@ -0,0 +1,28 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2010 Nokia Corporation + * Copyright (C) 2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +GSList *attrib_client_register(DBusConnection *connection, + struct btd_device *device, int psm, + GAttrib *attrib, GSList *primaries); +void attrib_client_unregister(GSList *services); diff --git a/attrib/gatt-service.c b/attrib/gatt-service.c new file mode 100644 index 0000000..a9de98c --- /dev/null +++ b/attrib/gatt-service.c @@ -0,0 +1,352 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2011 Nokia Corporation + * Copyright (C) 2011 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include + +#include "gattrib.h" +#include "att.h" +#include "gatt.h" +#include "att-database.h" +#include "attrib-server.h" +#include "gatt-service.h" +#include "log.h" + +struct gatt_info { + bt_uuid_t uuid; + uint8_t props; + int authentication; + int authorization; + GSList *callbacks; + unsigned int num_attrs; + uint16_t *value_handle; + uint16_t *ccc_handle; +}; + +struct attrib_cb { + attrib_event_t event; + void *fn; + void *user_data; +}; + +static GSList *parse_opts(gatt_option opt1, va_list args) +{ + gatt_option opt = opt1; + struct gatt_info *info; + struct attrib_cb *cb; + GSList *l = NULL; + + info = g_new0(struct gatt_info, 1); + l = g_slist_append(l, info); + + while (opt != GATT_OPT_INVALID) { + switch (opt) { + case GATT_OPT_CHR_UUID: + bt_uuid16_create(&info->uuid, va_arg(args, int)); + /* characteristic declaration and value */ + info->num_attrs += 2; + break; + case GATT_OPT_CHR_PROPS: + info->props = va_arg(args, int); + + if (info->props & (ATT_CHAR_PROPER_NOTIFY | + ATT_CHAR_PROPER_INDICATE)) + /* client characteristic configuration */ + info->num_attrs += 1; + + /* TODO: "Extended Properties" property requires a + * descriptor, but it is not supported yet. */ + break; + case GATT_OPT_CHR_VALUE_CB: + cb = g_new0(struct attrib_cb, 1); + cb->event = va_arg(args, attrib_event_t); + cb->fn = va_arg(args, void *); + cb->user_data = va_arg(args, void *); + info->callbacks = g_slist_append(info->callbacks, cb); + break; + case GATT_OPT_CHR_VALUE_GET_HANDLE: + info->value_handle = va_arg(args, void *); + break; + case GATT_OPT_CCC_GET_HANDLE: + info->ccc_handle = va_arg(args, void *); + break; + case GATT_OPT_CHR_AUTHENTICATION: + info->authentication = va_arg(args, gatt_option); + break; + case GATT_OPT_CHR_AUTHORIZATION: + info->authorization = va_arg(args, gatt_option); + break; + default: + error("Invalid option: %d", opt); + } + + opt = va_arg(args, gatt_option); + if (opt == GATT_OPT_CHR_UUID) { + info = g_new0(struct gatt_info, 1); + l = g_slist_append(l, info); + } + } + + return l; +} + +static struct attribute *add_service_declaration(struct btd_adapter *adapter, + uint16_t handle, uint16_t svc, bt_uuid_t *uuid) +{ + bt_uuid_t bt_uuid; + uint8_t atval[16]; + int len; + + if (uuid->type == BT_UUID16) { + att_put_u16(uuid->value.u16, &atval[0]); + len = 2; + } else if (uuid->type == BT_UUID128) { + att_put_u128(uuid->value.u128, &atval[0]); + len = 16; + } else + return NULL; + + bt_uuid16_create(&bt_uuid, svc); + + return attrib_db_add(adapter, handle, &bt_uuid, ATT_NONE, + ATT_NOT_PERMITTED, atval, len); +} + +static int att_read_reqs(int authorization, int authentication, uint8_t props) +{ + if (authorization == GATT_CHR_VALUE_READ || + authorization == GATT_CHR_VALUE_BOTH) + return ATT_AUTHORIZATION; + else if (authentication == GATT_CHR_VALUE_READ || + authentication == GATT_CHR_VALUE_BOTH) + return ATT_AUTHENTICATION; + else if (!(props & ATT_CHAR_PROPER_READ)) + return ATT_NOT_PERMITTED; + + return ATT_NONE; +} + +static int att_write_reqs(int authorization, int authentication, uint8_t props) +{ + if (authorization == GATT_CHR_VALUE_WRITE || + authorization == GATT_CHR_VALUE_BOTH) + return ATT_AUTHORIZATION; + else if (authentication == GATT_CHR_VALUE_WRITE || + authentication == GATT_CHR_VALUE_BOTH) + return ATT_AUTHENTICATION; + else if (!(props & (ATT_CHAR_PROPER_WRITE | + ATT_CHAR_PROPER_WRITE_WITHOUT_RESP))) + return ATT_NOT_PERMITTED; + + return ATT_NONE; +} + +static gint find_callback(gconstpointer a, gconstpointer b) +{ + const struct attrib_cb *cb = a; + unsigned int event = GPOINTER_TO_UINT(b); + + return cb->event - event; +} + +static gboolean add_characteristic(struct btd_adapter *adapter, + uint16_t *handle, struct gatt_info *info) +{ + int read_reqs, write_reqs; + uint16_t h = *handle; + struct attribute *a; + bt_uuid_t bt_uuid; + uint8_t atval[5]; + GSList *l; + + if (!info->uuid.value.u16 || !info->props) { + error("Characteristic UUID or properties are missing"); + return FALSE; + } + + read_reqs = att_read_reqs(info->authorization, info->authentication, + info->props); + write_reqs = att_write_reqs(info->authorization, info->authentication, + info->props); + + /* TODO: static characteristic values are not supported, therefore a + * callback must be always provided if a read/write property is set */ + if (read_reqs != ATT_NOT_PERMITTED) { + gpointer reqs = GUINT_TO_POINTER(ATTRIB_READ); + + if (!g_slist_find_custom(info->callbacks, reqs, + find_callback)) { + error("Callback for read required"); + return FALSE; + } + } + + if (write_reqs != ATT_NOT_PERMITTED) { + gpointer reqs = GUINT_TO_POINTER(ATTRIB_WRITE); + + if (!g_slist_find_custom(info->callbacks, reqs, + find_callback)) { + error("Callback for write required"); + return FALSE; + } + } + + /* characteristic declaration */ + bt_uuid16_create(&bt_uuid, GATT_CHARAC_UUID); + atval[0] = info->props; + att_put_u16(h + 1, &atval[1]); + att_put_u16(info->uuid.value.u16, &atval[3]); + if (attrib_db_add(adapter, h++, &bt_uuid, ATT_NONE, ATT_NOT_PERMITTED, + atval, sizeof(atval)) == NULL) + return FALSE; + + /* characteristic value */ + a = attrib_db_add(adapter, h++, &info->uuid, read_reqs, write_reqs, + NULL, 0); + if (a == NULL) + return FALSE; + + for (l = info->callbacks; l != NULL; l = l->next) { + struct attrib_cb *cb = l->data; + + switch (cb->event) { + case ATTRIB_READ: + a->read_cb = cb->fn; + break; + case ATTRIB_WRITE: + a->write_cb = cb->fn; + break; + } + + a->cb_user_data = cb->user_data; + } + + if (info->value_handle != NULL) + *info->value_handle = a->handle; + + /* client characteristic configuration descriptor */ + if (info->props & (ATT_CHAR_PROPER_NOTIFY | ATT_CHAR_PROPER_INDICATE)) { + uint8_t cfg_val[2]; + + bt_uuid16_create(&bt_uuid, GATT_CLIENT_CHARAC_CFG_UUID); + cfg_val[0] = 0x00; + cfg_val[1] = 0x00; + a = attrib_db_add(adapter, h++, &bt_uuid, ATT_NONE, + ATT_AUTHENTICATION, cfg_val, sizeof(cfg_val)); + if (a == NULL) + return FALSE; + + if (info->ccc_handle != NULL) + *info->ccc_handle = a->handle; + } + + *handle = h; + + return TRUE; +} + +static void free_gatt_info(void *data) +{ + struct gatt_info *info = data; + + g_slist_free_full(info->callbacks, g_free); + g_free(info); +} + +static void service_attr_del(struct btd_adapter *adapter, uint16_t start_handle, + uint16_t end_handle) +{ + uint16_t handle; + + for (handle = start_handle; handle <= end_handle; handle++) + if (attrib_db_del(adapter, handle) < 0) + error("Can't delete handle 0x%04x", handle); +} + +gboolean gatt_service_add(struct btd_adapter *adapter, uint16_t uuid, + bt_uuid_t *svc_uuid, gatt_option opt1, ...) +{ + char uuidstr[MAX_LEN_UUID_STR]; + uint16_t start_handle, h; + unsigned int size; + va_list args; + GSList *chrs, *l; + + bt_uuid_to_string(svc_uuid, uuidstr, MAX_LEN_UUID_STR); + + if (svc_uuid->type != BT_UUID16 && svc_uuid->type != BT_UUID128) { + error("Invalid service uuid: %s", uuidstr); + return FALSE; + } + + va_start(args, opt1); + chrs = parse_opts(opt1, args); + va_end(args); + + /* calculate how many attributes are necessary for this service */ + for (l = chrs, size = 1; l != NULL; l = l->next) { + struct gatt_info *info = l->data; + size += info->num_attrs; + } + + start_handle = attrib_db_find_avail(adapter, svc_uuid, size); + if (start_handle == 0) { + error("Not enough free handles to register service"); + goto fail; + } + + DBG("New service: handle 0x%04x, UUID %s, %d attributes", + start_handle, uuidstr, size); + + /* service declaration */ + h = start_handle; + if (add_service_declaration(adapter, h++, uuid, svc_uuid) == NULL) + goto fail; + + for (l = chrs; l != NULL; l = l->next) { + struct gatt_info *info = l->data; + + DBG("New characteristic: handle 0x%04x", h); + if (!add_characteristic(adapter, &h, info)) { + service_attr_del(adapter, start_handle, h - 1); + goto fail; + } + } + + g_assert(size < USHRT_MAX); + g_assert(h - start_handle == (uint16_t) size); + g_slist_free_full(chrs, free_gatt_info); + + return TRUE; + +fail: + g_slist_free_full(chrs, free_gatt_info); + return FALSE; +} diff --git a/attrib/gatt-service.h b/attrib/gatt-service.h new file mode 100644 index 0000000..b810e2e --- /dev/null +++ b/attrib/gatt-service.h @@ -0,0 +1,51 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2011 Nokia Corporation + * Copyright (C) 2011 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +typedef enum { + GATT_OPT_INVALID = 0, + GATT_OPT_CHR_UUID, + GATT_OPT_CHR_PROPS, + GATT_OPT_CHR_VALUE_CB, + GATT_OPT_CHR_AUTHENTICATION, + GATT_OPT_CHR_AUTHORIZATION, + + /* Get attribute handle for characteristic value */ + GATT_OPT_CHR_VALUE_GET_HANDLE, + + /* Get handle for ccc attribute */ + GATT_OPT_CCC_GET_HANDLE, + + /* arguments for authentication/authorization */ + GATT_CHR_VALUE_READ, + GATT_CHR_VALUE_WRITE, + GATT_CHR_VALUE_BOTH, +} gatt_option; + +typedef enum { + ATTRIB_READ, + ATTRIB_WRITE, +} attrib_event_t; + +gboolean gatt_service_add(struct btd_adapter *adapter, uint16_t uuid, + bt_uuid_t *svc_uuid, gatt_option opt1, ...); diff --git a/attrib/gatt.c b/attrib/gatt.c new file mode 100644 index 0000000..6f9a11d --- /dev/null +++ b/attrib/gatt.c @@ -0,0 +1,655 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2010 Nokia Corporation + * Copyright (C) 2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include "att.h" +#include "gattrib.h" +#include "gatt.h" + +struct discover_primary { + GAttrib *attrib; + bt_uuid_t uuid; + GSList *primaries; + gatt_cb_t cb; + void *user_data; +}; + +struct discover_char { + GAttrib *attrib; + bt_uuid_t *uuid; + uint16_t end; + GSList *characteristics; + gatt_cb_t cb; + void *user_data; +}; + +static void discover_primary_free(struct discover_primary *dp) +{ + g_slist_free(dp->primaries); + g_attrib_unref(dp->attrib); + g_free(dp); +} + +static void discover_char_free(struct discover_char *dc) +{ + g_slist_free_full(dc->characteristics, g_free); + g_attrib_unref(dc->attrib); + g_free(dc->uuid); + g_free(dc); +} + +static guint16 encode_discover_primary(uint16_t start, uint16_t end, + bt_uuid_t *uuid, uint8_t *pdu, size_t len) +{ + bt_uuid_t prim; + guint16 plen; + + bt_uuid16_create(&prim, GATT_PRIM_SVC_UUID); + + if (uuid == NULL) { + /* Discover all primary services */ + plen = enc_read_by_grp_req(start, end, &prim, pdu, len); + } else { + uint16_t u16; + uint128_t u128; + const void *value; + int vlen; + + /* Discover primary service by service UUID */ + + if (uuid->type == BT_UUID16) { + u16 = htobs(uuid->value.u16); + value = &u16; + vlen = sizeof(u16); + } else { + htob128(&uuid->value.u128, &u128); + value = &u128; + vlen = sizeof(u128); + } + + plen = enc_find_by_type_req(start, end, &prim, value, vlen, + pdu, len); + } + + return plen; +} + +static void primary_by_uuid_cb(guint8 status, const guint8 *ipdu, + guint16 iplen, gpointer user_data) + +{ + struct discover_primary *dp = user_data; + GSList *ranges, *last; + struct att_range *range; + uint8_t *buf; + guint16 oplen; + int err = 0, buflen; + + if (status) { + err = status == ATT_ECODE_ATTR_NOT_FOUND ? 0 : status; + goto done; + } + + ranges = dec_find_by_type_resp(ipdu, iplen); + if (ranges == NULL) + goto done; + + dp->primaries = g_slist_concat(dp->primaries, ranges); + + last = g_slist_last(ranges); + range = last->data; + + if (range->end == 0xffff) + goto done; + + buf = g_attrib_get_buffer(dp->attrib, &buflen); + oplen = encode_discover_primary(range->end + 1, 0xffff, &dp->uuid, + buf, buflen); + + if (oplen == 0) + goto done; + + g_attrib_send(dp->attrib, 0, buf[0], buf, oplen, primary_by_uuid_cb, + dp, NULL); + return; + +done: + dp->cb(dp->primaries, err, dp->user_data); + discover_primary_free(dp); +} + +static void primary_all_cb(guint8 status, const guint8 *ipdu, guint16 iplen, + gpointer user_data) +{ + struct discover_primary *dp = user_data; + struct att_data_list *list; + unsigned int i, err; + uint16_t start, end; + + if (status) { + err = status == ATT_ECODE_ATTR_NOT_FOUND ? 0 : status; + goto done; + } + + list = dec_read_by_grp_resp(ipdu, iplen); + if (list == NULL) { + err = ATT_ECODE_IO; + goto done; + } + + for (i = 0, end = 0; i < list->num; i++) { + const uint8_t *data = list->data[i]; + struct gatt_primary *primary; + bt_uuid_t uuid; + + start = att_get_u16(&data[0]); + end = att_get_u16(&data[2]); + + if (list->len == 6) { + bt_uuid_t uuid16 = att_get_uuid16(&data[4]); + bt_uuid_to_uuid128(&uuid16, &uuid); + } else if (list->len == 20) { + uuid = att_get_uuid128(&data[4]); + } else { + /* Skipping invalid data */ + continue; + } + + primary = g_try_new0(struct gatt_primary, 1); + if (!primary) { + err = ATT_ECODE_INSUFF_RESOURCES; + goto done; + } + primary->range.start = start; + primary->range.end = end; + bt_uuid_to_string(&uuid, primary->uuid, sizeof(primary->uuid)); + dp->primaries = g_slist_append(dp->primaries, primary); + } + + att_data_list_free(list); + err = 0; + + if (end != 0xffff) { + int buflen; + uint8_t *buf = g_attrib_get_buffer(dp->attrib, &buflen); + guint16 oplen = encode_discover_primary(end + 1, 0xffff, NULL, + buf, buflen); + + g_attrib_send(dp->attrib, 0, buf[0], buf, oplen, primary_all_cb, + dp, NULL); + + return; + } + +done: + dp->cb(dp->primaries, err, dp->user_data); + discover_primary_free(dp); +} + +guint gatt_discover_primary(GAttrib *attrib, bt_uuid_t *uuid, gatt_cb_t func, + gpointer user_data) +{ + struct discover_primary *dp; + int buflen; + uint8_t *buf = g_attrib_get_buffer(attrib, &buflen); + GAttribResultFunc cb; + guint16 plen; + + plen = encode_discover_primary(0x0001, 0xffff, uuid, buf, buflen); + if (plen == 0) + return 0; + + dp = g_try_new0(struct discover_primary, 1); + if (dp == NULL) + return 0; + + dp->attrib = g_attrib_ref(attrib); + dp->cb = func; + dp->user_data = user_data; + + if (uuid) { + dp->uuid = *uuid; + cb = primary_by_uuid_cb; + } else + cb = primary_all_cb; + + return g_attrib_send(attrib, 0, buf[0], buf, plen, cb, dp, NULL); +} + +static void char_discovered_cb(guint8 status, const guint8 *ipdu, guint16 iplen, + gpointer user_data) +{ + struct discover_char *dc = user_data; + struct att_data_list *list; + unsigned int i, err; + int buflen; + uint8_t *buf; + guint16 oplen; + bt_uuid_t uuid; + uint16_t last = 0; + + if (status) { + err = status == ATT_ECODE_ATTR_NOT_FOUND ? 0 : status; + goto done; + } + + list = dec_read_by_type_resp(ipdu, iplen); + if (list == NULL) { + err = ATT_ECODE_IO; + goto done; + } + + for (i = 0; i < list->num; i++) { + uint8_t *value = list->data[i]; + struct gatt_char *chars; + bt_uuid_t uuid; + + last = att_get_u16(value); + + if (list->len == 7) { + bt_uuid_t uuid16 = att_get_uuid16(&value[5]); + bt_uuid_to_uuid128(&uuid16, &uuid); + } else + uuid = att_get_uuid128(&value[5]); + + chars = g_try_new0(struct gatt_char, 1); + if (!chars) { + err = ATT_ECODE_INSUFF_RESOURCES; + goto done; + } + + if (dc->uuid && bt_uuid_cmp(dc->uuid, &uuid)) + break; + + chars->handle = last; + chars->properties = value[2]; + chars->value_handle = att_get_u16(&value[3]); + bt_uuid_to_string(&uuid, chars->uuid, sizeof(chars->uuid)); + dc->characteristics = g_slist_append(dc->characteristics, + chars); + } + + att_data_list_free(list); + err = 0; + + if (last != 0 && (last + 1 < dc->end)) { + buf = g_attrib_get_buffer(dc->attrib, &buflen); + + bt_uuid16_create(&uuid, GATT_CHARAC_UUID); + + oplen = enc_read_by_type_req(last + 1, dc->end, &uuid, buf, + buflen); + + if (oplen == 0) + return; + + g_attrib_send(dc->attrib, 0, buf[0], buf, oplen, + char_discovered_cb, dc, NULL); + + return; + } + +done: + dc->cb(dc->characteristics, err, dc->user_data); + discover_char_free(dc); +} + +guint gatt_discover_char(GAttrib *attrib, uint16_t start, uint16_t end, + bt_uuid_t *uuid, gatt_cb_t func, + gpointer user_data) +{ + int buflen; + uint8_t *buf = g_attrib_get_buffer(attrib, &buflen); + struct discover_char *dc; + bt_uuid_t type_uuid; + guint16 plen; + + bt_uuid16_create(&type_uuid, GATT_CHARAC_UUID); + + plen = enc_read_by_type_req(start, end, &type_uuid, buf, buflen); + if (plen == 0) + return 0; + + dc = g_try_new0(struct discover_char, 1); + if (dc == NULL) + return 0; + + dc->attrib = g_attrib_ref(attrib); + dc->cb = func; + dc->user_data = user_data; + dc->end = end; + dc->uuid = g_memdup(uuid, sizeof(bt_uuid_t)); + + return g_attrib_send(attrib, 0, buf[0], buf, plen, char_discovered_cb, + dc, NULL); +} + +guint gatt_read_char_by_uuid(GAttrib *attrib, uint16_t start, uint16_t end, + bt_uuid_t *uuid, GAttribResultFunc func, + gpointer user_data) +{ + int buflen; + uint8_t *buf = g_attrib_get_buffer(attrib, &buflen); + guint16 plen; + + plen = enc_read_by_type_req(start, end, uuid, buf, buflen); + if (plen == 0) + return 0; + + return g_attrib_send(attrib, 0, ATT_OP_READ_BY_TYPE_REQ, + buf, plen, func, user_data, NULL); +} + +struct read_long_data { + GAttrib *attrib; + GAttribResultFunc func; + gpointer user_data; + guint8 *buffer; + guint16 size; + guint16 handle; + guint id; + gint ref; +}; + +static void read_long_destroy(gpointer user_data) +{ + struct read_long_data *long_read = user_data; + + if (g_atomic_int_dec_and_test(&long_read->ref) == FALSE) + return; + + if (long_read->buffer != NULL) + g_free(long_read->buffer); + + g_free(long_read); +} + +static void read_blob_helper(guint8 status, const guint8 *rpdu, guint16 rlen, + gpointer user_data) +{ + struct read_long_data *long_read = user_data; + uint8_t *buf; + int buflen; + guint8 *tmp; + guint16 plen; + guint id; + + if (status != 0 || rlen == 1) { + status = 0; + goto done; + } + + tmp = g_try_realloc(long_read->buffer, long_read->size + rlen - 1); + + if (tmp == NULL) { + status = ATT_ECODE_INSUFF_RESOURCES; + goto done; + } + + memcpy(&tmp[long_read->size], &rpdu[1], rlen - 1); + long_read->buffer = tmp; + long_read->size += rlen - 1; + + buf = g_attrib_get_buffer(long_read->attrib, &buflen); + if (rlen < buflen) + goto done; + + plen = enc_read_blob_req(long_read->handle, long_read->size - 1, + buf, buflen); + id = g_attrib_send(long_read->attrib, long_read->id, + ATT_OP_READ_BLOB_REQ, buf, plen, + read_blob_helper, long_read, read_long_destroy); + + if (id != 0) { + g_atomic_int_inc(&long_read->ref); + return; + } + + status = ATT_ECODE_IO; + +done: + long_read->func(status, long_read->buffer, long_read->size, + long_read->user_data); +} + +static void read_char_helper(guint8 status, const guint8 *rpdu, + guint16 rlen, gpointer user_data) +{ + struct read_long_data *long_read = user_data; + int buflen; + uint8_t *buf = g_attrib_get_buffer(long_read->attrib, &buflen); + guint16 plen; + guint id; + + if (status != 0 || rlen < buflen) + goto done; + + long_read->buffer = g_malloc(rlen); + + if (long_read->buffer == NULL) + goto done; + + memcpy(long_read->buffer, rpdu, rlen); + long_read->size = rlen; + + plen = enc_read_blob_req(long_read->handle, rlen - 1, buf, buflen); + id = g_attrib_send(long_read->attrib, long_read->id, + ATT_OP_READ_BLOB_REQ, buf, plen, read_blob_helper, + long_read, read_long_destroy); + + if (id != 0) { + g_atomic_int_inc(&long_read->ref); + return; + } + + status = ATT_ECODE_IO; + +done: + long_read->func(status, rpdu, rlen, long_read->user_data); +} + +guint gatt_read_char(GAttrib *attrib, uint16_t handle, uint16_t offset, + GAttribResultFunc func, gpointer user_data) +{ + uint8_t *buf; + int buflen; + guint16 plen; + guint id; + struct read_long_data *long_read; + + long_read = g_try_new0(struct read_long_data, 1); + + if (long_read == NULL) + return 0; + + long_read->attrib = attrib; + long_read->func = func; + long_read->user_data = user_data; + long_read->handle = handle; + + buf = g_attrib_get_buffer(attrib, &buflen); + if (offset > 0) { + plen = enc_read_blob_req(long_read->handle, offset, buf, + buflen); + id = g_attrib_send(attrib, 0, ATT_OP_READ_BLOB_REQ, buf, plen, + read_blob_helper, long_read, read_long_destroy); + } else { + plen = enc_read_req(handle, buf, buflen); + id = g_attrib_send(attrib, 0, ATT_OP_READ_REQ, buf, plen, + read_char_helper, long_read, read_long_destroy); + } + + if (id == 0) + g_free(long_read); + else { + g_atomic_int_inc(&long_read->ref); + long_read->id = id; + } + + return id; +} + +guint gatt_write_char(GAttrib *attrib, uint16_t handle, uint8_t *value, + int vlen, GAttribResultFunc func, gpointer user_data) +{ + uint8_t *buf; + int buflen; + guint16 plen; + + buf = g_attrib_get_buffer(attrib, &buflen); + if (func) + plen = enc_write_req(handle, value, vlen, buf, buflen); + else + plen = enc_write_cmd(handle, value, vlen, buf, buflen); + + return g_attrib_send(attrib, 0, buf[0], buf, plen, func, + user_data, NULL); +} + +guint gatt_exchange_mtu(GAttrib *attrib, uint16_t mtu, GAttribResultFunc func, + gpointer user_data) +{ + uint8_t *buf; + int buflen; + guint16 plen; + + buf = g_attrib_get_buffer(attrib, &buflen); + plen = enc_mtu_req(mtu, buf, buflen); + return g_attrib_send(attrib, 0, ATT_OP_MTU_REQ, buf, plen, func, + user_data, NULL); +} + +guint gatt_find_info(GAttrib *attrib, uint16_t start, uint16_t end, + GAttribResultFunc func, gpointer user_data) +{ + uint8_t *buf; + int buflen; + guint16 plen; + + buf = g_attrib_get_buffer(attrib, &buflen); + plen = enc_find_info_req(start, end, buf, buflen); + if (plen == 0) + return 0; + + return g_attrib_send(attrib, 0, ATT_OP_FIND_INFO_REQ, buf, plen, func, + user_data, NULL); +} + +guint gatt_write_cmd(GAttrib *attrib, uint16_t handle, uint8_t *value, int vlen, + GDestroyNotify notify, gpointer user_data) +{ + uint8_t *buf; + int buflen; + guint16 plen; + + buf = g_attrib_get_buffer(attrib, &buflen); + plen = enc_write_cmd(handle, value, vlen, buf, buflen); + return g_attrib_send(attrib, 0, ATT_OP_WRITE_CMD, buf, plen, NULL, + user_data, notify); +} + +static sdp_data_t *proto_seq_find(sdp_list_t *proto_list) +{ + sdp_list_t *list; + uuid_t proto; + + sdp_uuid16_create(&proto, ATT_UUID); + + for (list = proto_list; list; list = list->next) { + sdp_list_t *p; + for (p = list->data; p; p = p->next) { + sdp_data_t *seq = p->data; + if (seq && seq->dtd == SDP_UUID16 && + sdp_uuid16_cmp(&proto, &seq->val.uuid) == 0) + return seq->next; + } + } + + return NULL; +} + +static gboolean parse_proto_params(sdp_list_t *proto_list, uint16_t *psm, + uint16_t *start, uint16_t *end) +{ + sdp_data_t *seq1, *seq2; + + if (psm) + *psm = sdp_get_proto_port(proto_list, L2CAP_UUID); + + /* Getting start and end handle */ + seq1 = proto_seq_find(proto_list); + if (!seq1 || seq1->dtd != SDP_UINT16) + return FALSE; + + seq2 = seq1->next; + if (!seq2 || seq2->dtd != SDP_UINT16) + return FALSE; + + if (start) + *start = seq1->val.uint16; + + if (end) + *end = seq2->val.uint16; + + return TRUE; +} + +gboolean gatt_parse_record(const sdp_record_t *rec, + uuid_t *prim_uuid, uint16_t *psm, + uint16_t *start, uint16_t *end) +{ + sdp_list_t *list; + uuid_t uuid; + gboolean ret; + + if (sdp_get_service_classes(rec, &list) < 0) + return FALSE; + + memcpy(&uuid, list->data, sizeof(uuid)); + sdp_list_free(list, free); + + if (sdp_get_access_protos(rec, &list) < 0) + return FALSE; + + ret = parse_proto_params(list, psm, start, end); + + sdp_list_foreach(list, (sdp_list_func_t) sdp_list_free, NULL); + sdp_list_free(list, NULL); + + /* FIXME: replace by bt_uuid_t after uuid_t/sdp code cleanup */ + if (ret && prim_uuid) + memcpy(prim_uuid, &uuid, sizeof(uuid_t)); + + return ret; +} diff --git a/attrib/gatt.h b/attrib/gatt.h new file mode 100644 index 0000000..9ffe58f --- /dev/null +++ b/attrib/gatt.h @@ -0,0 +1,96 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2010 Nokia Corporation + * Copyright (C) 2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include + +/* GATT Profile Attribute types */ +#define GATT_PRIM_SVC_UUID 0x2800 +#define GATT_SND_SVC_UUID 0x2801 +#define GATT_INCLUDE_UUID 0x2802 +#define GATT_CHARAC_UUID 0x2803 + +/* GATT Characteristic Types */ +#define GATT_CHARAC_DEVICE_NAME 0x2A00 +#define GATT_CHARAC_APPEARANCE 0x2A01 +#define GATT_CHARAC_PERIPHERAL_PRIV_FLAG 0x2A02 +#define GATT_CHARAC_RECONNECTION_ADDRESS 0x2A03 +#define GATT_CHARAC_PERIPHERAL_PREF_CONN 0x2A04 +#define GATT_CHARAC_SERVICE_CHANGED 0x2A05 + +/* GATT Characteristic Descriptors */ +#define GATT_CHARAC_EXT_PROPER_UUID 0x2900 +#define GATT_CHARAC_USER_DESC_UUID 0x2901 +#define GATT_CLIENT_CHARAC_CFG_UUID 0x2902 +#define GATT_SERVER_CHARAC_CFG_UUID 0x2903 +#define GATT_CHARAC_FMT_UUID 0x2904 +#define GATT_CHARAC_AGREG_FMT_UUID 0x2905 +#define GATT_CHARAC_VALID_RANGE_UUID 0x2906 + +/* Client Characteristic Configuration bit field */ +#define GATT_CLIENT_CHARAC_CFG_NOTIF_BIT 0x0001 +#define GATT_CLIENT_CHARAC_CFG_IND_BIT 0x0002 + +typedef void (*gatt_cb_t) (GSList *l, guint8 status, gpointer user_data); + +struct gatt_primary { + char uuid[MAX_LEN_UUID_STR + 1]; + struct att_range range; +}; + +struct gatt_char { + char uuid[MAX_LEN_UUID_STR + 1]; + uint16_t handle; + uint8_t properties; + uint16_t value_handle; +}; + +guint gatt_discover_primary(GAttrib *attrib, bt_uuid_t *uuid, gatt_cb_t func, + gpointer user_data); + +guint gatt_discover_char(GAttrib *attrib, uint16_t start, uint16_t end, + bt_uuid_t *uuid, gatt_cb_t func, + gpointer user_data); + +guint gatt_read_char(GAttrib *attrib, uint16_t handle, uint16_t offset, + GAttribResultFunc func, gpointer user_data); + +guint gatt_write_char(GAttrib *attrib, uint16_t handle, uint8_t *value, + int vlen, GAttribResultFunc func, gpointer user_data); + +guint gatt_find_info(GAttrib *attrib, uint16_t start, uint16_t end, + GAttribResultFunc func, gpointer user_data); + +guint gatt_write_cmd(GAttrib *attrib, uint16_t handle, uint8_t *value, int vlen, + GDestroyNotify notify, gpointer user_data); + +guint gatt_read_char_by_uuid(GAttrib *attrib, uint16_t start, uint16_t end, + bt_uuid_t *uuid, GAttribResultFunc func, + gpointer user_data); + +guint gatt_exchange_mtu(GAttrib *attrib, uint16_t mtu, GAttribResultFunc func, + gpointer user_data); + +gboolean gatt_parse_record(const sdp_record_t *rec, + uuid_t *prim_uuid, uint16_t *psm, + uint16_t *start, uint16_t *end); diff --git a/attrib/gattrib.c b/attrib/gattrib.c new file mode 100644 index 0000000..00f59d7 --- /dev/null +++ b/attrib/gattrib.c @@ -0,0 +1,726 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2010 Nokia Corporation + * Copyright (C) 2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include +#include + +#include + +#include +#include + +#include "log.h" +#include "att.h" +#include "btio.h" +#include "gattrib.h" + +#define GATT_TIMEOUT 30 + +struct _GAttrib { + GIOChannel *io; + gint refs; + uint8_t *buf; + int buflen; + guint read_watch; + guint write_watch; + guint timeout_watch; + GQueue *requests; + GQueue *responses; + GSList *events; + guint next_cmd_id; + GDestroyNotify destroy; + gpointer destroy_user_data; + gboolean stale; +}; + +struct command { + guint id; + guint8 opcode; + guint8 *pdu; + guint16 len; + guint8 expected; + gboolean sent; + GAttribResultFunc func; + gpointer user_data; + GDestroyNotify notify; +}; + +struct event { + guint id; + guint8 expected; + GAttribNotifyFunc func; + gpointer user_data; + GDestroyNotify notify; +}; + +static guint8 opcode2expected(guint8 opcode) +{ + switch (opcode) { + case ATT_OP_MTU_REQ: + return ATT_OP_MTU_RESP; + + case ATT_OP_FIND_INFO_REQ: + return ATT_OP_FIND_INFO_RESP; + + case ATT_OP_FIND_BY_TYPE_REQ: + return ATT_OP_FIND_BY_TYPE_RESP; + + case ATT_OP_READ_BY_TYPE_REQ: + return ATT_OP_READ_BY_TYPE_RESP; + + case ATT_OP_READ_REQ: + return ATT_OP_READ_RESP; + + case ATT_OP_READ_BLOB_REQ: + return ATT_OP_READ_BLOB_RESP; + + case ATT_OP_READ_MULTI_REQ: + return ATT_OP_READ_MULTI_RESP; + + case ATT_OP_READ_BY_GROUP_REQ: + return ATT_OP_READ_BY_GROUP_RESP; + + case ATT_OP_WRITE_REQ: + return ATT_OP_WRITE_RESP; + + case ATT_OP_PREP_WRITE_REQ: + return ATT_OP_PREP_WRITE_RESP; + + case ATT_OP_EXEC_WRITE_REQ: + return ATT_OP_EXEC_WRITE_RESP; + + case ATT_OP_HANDLE_IND: + return ATT_OP_HANDLE_CNF; + } + + return 0; +} + +static gboolean is_response(guint8 opcode) +{ + switch (opcode) { + case ATT_OP_ERROR: + case ATT_OP_MTU_RESP: + case ATT_OP_FIND_INFO_RESP: + case ATT_OP_FIND_BY_TYPE_RESP: + case ATT_OP_READ_BY_TYPE_RESP: + case ATT_OP_READ_RESP: + case ATT_OP_READ_BLOB_RESP: + case ATT_OP_READ_MULTI_RESP: + case ATT_OP_READ_BY_GROUP_RESP: + case ATT_OP_WRITE_RESP: + case ATT_OP_PREP_WRITE_RESP: + case ATT_OP_EXEC_WRITE_RESP: + case ATT_OP_HANDLE_CNF: + return TRUE; + } + + return FALSE; +} + +GAttrib *g_attrib_ref(GAttrib *attrib) +{ + if (!attrib) + return NULL; + + g_atomic_int_inc(&attrib->refs); + + DBG("%p: ref=%d", attrib, attrib->refs); + + return attrib; +} + +static void command_destroy(struct command *cmd) +{ + if (cmd->notify) + cmd->notify(cmd->user_data); + + g_free(cmd->pdu); + g_free(cmd); +} + +static void event_destroy(struct event *evt) +{ + if (evt->notify) + evt->notify(evt->user_data); + + g_free(evt); +} + +static void attrib_destroy(GAttrib *attrib) +{ + GSList *l; + struct command *c; + + while ((c = g_queue_pop_head(attrib->requests))) + command_destroy(c); + + while ((c = g_queue_pop_head(attrib->responses))) + command_destroy(c); + + g_queue_free(attrib->requests); + attrib->requests = NULL; + + g_queue_free(attrib->responses); + attrib->responses = NULL; + + for (l = attrib->events; l; l = l->next) + event_destroy(l->data); + + g_slist_free(attrib->events); + attrib->events = NULL; + + if (attrib->timeout_watch > 0) + g_source_remove(attrib->timeout_watch); + + if (attrib->write_watch > 0) + g_source_remove(attrib->write_watch); + + if (attrib->read_watch > 0) + g_source_remove(attrib->read_watch); + + if (attrib->io) + g_io_channel_unref(attrib->io); + + g_free(attrib->buf); + + if (attrib->destroy) + attrib->destroy(attrib->destroy_user_data); + + g_free(attrib); +} + +void g_attrib_unref(GAttrib *attrib) +{ + gboolean ret; + + if (!attrib) + return; + + ret = g_atomic_int_dec_and_test(&attrib->refs); + + DBG("%p: ref=%d", attrib, attrib->refs); + + if (ret == FALSE) + return; + + attrib_destroy(attrib); +} + +GIOChannel *g_attrib_get_channel(GAttrib *attrib) +{ + if (!attrib) + return NULL; + + return attrib->io; +} + +gboolean g_attrib_set_destroy_function(GAttrib *attrib, + GDestroyNotify destroy, gpointer user_data) +{ + if (attrib == NULL) + return FALSE; + + attrib->destroy = destroy; + attrib->destroy_user_data = user_data; + + return TRUE; +} + +static gboolean disconnect_timeout(gpointer data) +{ + struct _GAttrib *attrib = data; + struct command *c; + + g_attrib_ref(attrib); + + c = g_queue_pop_head(attrib->requests); + if (c == NULL) + goto done; + + if (c->func) + c->func(ATT_ECODE_TIMEOUT, NULL, 0, c->user_data); + + command_destroy(c); + + while ((c = g_queue_pop_head(attrib->requests))) { + if (c->func) + c->func(ATT_ECODE_ABORTED, NULL, 0, c->user_data); + command_destroy(c); + } + +done: + attrib->stale = TRUE; + + g_attrib_unref(attrib); + + return FALSE; +} + +static gboolean can_write_data(GIOChannel *io, GIOCondition cond, + gpointer data) +{ + struct _GAttrib *attrib = data; + struct command *cmd; + GError *gerr = NULL; + gsize len; + GIOStatus iostat; + GQueue *queue; + + if (attrib->stale) + return FALSE; + + if (cond & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) + return FALSE; + + queue = attrib->responses; + cmd = g_queue_peek_head(queue); + if (cmd == NULL) { + queue = attrib->requests; + cmd = g_queue_peek_head(queue); + } + if (cmd == NULL) + return FALSE; + + /* + * Verify that we didn't already send this command. This can only + * happen with elementes from attrib->requests. + */ + if (cmd->sent) + return FALSE; + + iostat = g_io_channel_write_chars(io, (gchar *) cmd->pdu, cmd->len, + &len, &gerr); + if (iostat != G_IO_STATUS_NORMAL) + return FALSE; + + if (cmd->expected == 0) { + g_queue_pop_head(queue); + command_destroy(cmd); + + return TRUE; + } + + cmd->sent = TRUE; + + if (attrib->timeout_watch == 0) + attrib->timeout_watch = g_timeout_add_seconds(GATT_TIMEOUT, + disconnect_timeout, attrib); + + return FALSE; +} + +static void destroy_sender(gpointer data) +{ + struct _GAttrib *attrib = data; + + attrib->write_watch = 0; + g_attrib_unref(attrib); +} + +static void wake_up_sender(struct _GAttrib *attrib) +{ + if (attrib->write_watch > 0) + return; + + attrib = g_attrib_ref(attrib); + attrib->write_watch = g_io_add_watch_full(attrib->io, + G_PRIORITY_DEFAULT, G_IO_OUT, + can_write_data, attrib, destroy_sender); +} + +static gboolean received_data(GIOChannel *io, GIOCondition cond, gpointer data) +{ + struct _GAttrib *attrib = data; + struct command *cmd = NULL; + GSList *l; + uint8_t buf[512], status; + gsize len; + GIOStatus iostat; + gboolean norequests, noresponses; + + if (attrib->stale) + return FALSE; + + if (cond & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) { + attrib->read_watch = 0; + return FALSE; + } + + memset(buf, 0, sizeof(buf)); + + iostat = g_io_channel_read_chars(io, (gchar *) buf, sizeof(buf), + &len, NULL); + if (iostat != G_IO_STATUS_NORMAL) { + status = ATT_ECODE_IO; + goto done; + } + + for (l = attrib->events; l; l = l->next) { + struct event *evt = l->data; + + if (evt->expected == buf[0] || + evt->expected == GATTRIB_ALL_EVENTS || + (is_response(buf[0]) == FALSE && + evt->expected == GATTRIB_ALL_REQS)) + evt->func(buf, len, evt->user_data); + } + + if (is_response(buf[0]) == FALSE) + return TRUE; + + if (attrib->timeout_watch > 0) { + g_source_remove(attrib->timeout_watch); + attrib->timeout_watch = 0; + } + + cmd = g_queue_pop_head(attrib->requests); + if (cmd == NULL) { + /* Keep the watch if we have events to report */ + return attrib->events != NULL; + } + + if (buf[0] == ATT_OP_ERROR) { + status = buf[4]; + goto done; + } + + if (cmd->expected != buf[0]) { + status = ATT_ECODE_IO; + goto done; + } + + status = 0; + +done: + norequests = attrib->requests == NULL || + g_queue_is_empty(attrib->requests); + noresponses = attrib->responses == NULL || + g_queue_is_empty(attrib->responses); + + if (cmd) { + if (cmd->func) + cmd->func(status, buf, len, cmd->user_data); + + command_destroy(cmd); + } + + if (!norequests || !noresponses) + wake_up_sender(attrib); + + return TRUE; +} + +GAttrib *g_attrib_new(GIOChannel *io) +{ + struct _GAttrib *attrib; + uint16_t imtu; + uint16_t att_mtu; + uint16_t cid; + GError *gerr = NULL; + + g_io_channel_set_encoding(io, NULL, NULL); + g_io_channel_set_buffered(io, FALSE); + + bt_io_get(io, BT_IO_L2CAP, &gerr, + BT_IO_OPT_IMTU, &imtu, + BT_IO_OPT_CID, &cid, + BT_IO_OPT_INVALID); + + if (gerr) { + error("%s", gerr->message); + g_error_free(gerr); + return NULL; + } + + attrib = g_try_new0(struct _GAttrib, 1); + if (attrib == NULL) + return NULL; + + att_mtu = (cid == ATT_CID) ? ATT_DEFAULT_LE_MTU : imtu; + + attrib->buf = g_malloc0(att_mtu); + attrib->buflen = att_mtu; + + attrib->io = g_io_channel_ref(io); + attrib->requests = g_queue_new(); + attrib->responses = g_queue_new(); + + attrib->read_watch = g_io_add_watch(attrib->io, + G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, + received_data, attrib); + + return g_attrib_ref(attrib); +} + +guint g_attrib_send(GAttrib *attrib, guint id, guint8 opcode, + const guint8 *pdu, guint16 len, GAttribResultFunc func, + gpointer user_data, GDestroyNotify notify) +{ + struct command *c; + GQueue *queue; + + if (attrib->stale) + return 0; + + c = g_try_new0(struct command, 1); + if (c == NULL) + return 0; + + c->opcode = opcode; + c->expected = opcode2expected(opcode); + c->pdu = g_malloc(len); + memcpy(c->pdu, pdu, len); + c->len = len; + c->func = func; + c->user_data = user_data; + c->notify = notify; + + if (is_response(opcode)) + queue = attrib->responses; + else + queue = attrib->requests; + + if (id) { + c->id = id; + if (!is_response(opcode)) + g_queue_push_head(queue, c); + else + /* Don't re-order responses even if an ID is given */ + g_queue_push_tail(queue, c); + } else { + c->id = ++attrib->next_cmd_id; + g_queue_push_tail(queue, c); + } + + /* + * If a command was added to the queue and it was empty before, wake up + * the sender. If the sender was already woken up by the second queue, + * wake_up_sender will just return. + */ + if (g_queue_get_length(queue) == 1) + wake_up_sender(attrib); + + return c->id; +} + +static gint command_cmp_by_id(gconstpointer a, gconstpointer b) +{ + const struct command *cmd = a; + guint id = GPOINTER_TO_UINT(b); + + return cmd->id - id; +} + +gboolean g_attrib_cancel(GAttrib *attrib, guint id) +{ + GList *l = NULL; + struct command *cmd; + GQueue *queue; + + if (attrib == NULL) + return FALSE; + + queue = attrib->requests; + if (queue) + l = g_queue_find_custom(queue, GUINT_TO_POINTER(id), + command_cmp_by_id); + if (l == NULL) { + queue = attrib->responses; + if (!queue) + return FALSE; + l = g_queue_find_custom(queue, GUINT_TO_POINTER(id), + command_cmp_by_id); + } + + if (l == NULL) + return FALSE; + + cmd = l->data; + + if (cmd == g_queue_peek_head(queue) && cmd->sent) + cmd->func = NULL; + else { + g_queue_remove(queue, cmd); + command_destroy(cmd); + } + + return TRUE; +} + +static gboolean cancel_all_per_queue(GQueue *queue) +{ + struct command *c, *head = NULL; + gboolean first = TRUE; + + if (queue == NULL) + return FALSE; + + while ((c = g_queue_pop_head(queue))) { + if (first && c->sent) { + /* If the command was sent ignore its callback ... */ + c->func = NULL; + head = c; + continue; + } + + first = FALSE; + command_destroy(c); + } + + if (head) { + /* ... and put it back in the queue */ + g_queue_push_head(queue, head); + } + + return TRUE; +} + +gboolean g_attrib_cancel_all(GAttrib *attrib) +{ + gboolean ret; + + if (attrib == NULL) + return FALSE; + + ret = cancel_all_per_queue(attrib->requests); + ret = cancel_all_per_queue(attrib->responses) && ret; + + return ret; +} + +gboolean g_attrib_set_debug(GAttrib *attrib, + GAttribDebugFunc func, gpointer user_data) +{ + return TRUE; +} + +uint8_t *g_attrib_get_buffer(GAttrib *attrib, int *len) +{ + if (len == NULL) + return NULL; + + *len = attrib->buflen; + + return attrib->buf; +} + +gboolean g_attrib_set_mtu(GAttrib *attrib, int mtu) +{ + if (mtu < ATT_DEFAULT_LE_MTU) + return FALSE; + + attrib->buf = g_realloc(attrib->buf, mtu); + + attrib->buflen = mtu; + + return TRUE; +} + +guint g_attrib_register(GAttrib *attrib, guint8 opcode, + GAttribNotifyFunc func, gpointer user_data, + GDestroyNotify notify) +{ + static guint next_evt_id = 0; + struct event *event; + + event = g_try_new0(struct event, 1); + if (event == NULL) + return 0; + + event->expected = opcode; + event->func = func; + event->user_data = user_data; + event->notify = notify; + event->id = ++next_evt_id; + + attrib->events = g_slist_append(attrib->events, event); + + return event->id; +} + +static gint event_cmp_by_id(gconstpointer a, gconstpointer b) +{ + const struct event *evt = a; + guint id = GPOINTER_TO_UINT(b); + + return evt->id - id; +} + +gboolean g_attrib_is_encrypted(GAttrib *attrib) +{ + BtIOSecLevel sec_level; + + if (!bt_io_get(attrib->io, BT_IO_L2CAP, NULL, + BT_IO_OPT_SEC_LEVEL, &sec_level, + BT_IO_OPT_INVALID)) + return FALSE; + + return sec_level > BT_IO_SEC_LOW; +} + +gboolean g_attrib_unregister(GAttrib *attrib, guint id) +{ + struct event *evt; + GSList *l; + + l = g_slist_find_custom(attrib->events, GUINT_TO_POINTER(id), + event_cmp_by_id); + if (l == NULL) + return FALSE; + + evt = l->data; + + attrib->events = g_slist_remove(attrib->events, evt); + + if (evt->notify) + evt->notify(evt->user_data); + + g_free(evt); + + return TRUE; +} + +gboolean g_attrib_unregister_all(GAttrib *attrib) +{ + GSList *l; + + if (attrib->events == NULL) + return FALSE; + + for (l = attrib->events; l; l = l->next) { + struct event *evt = l->data; + + if (evt->notify) + evt->notify(evt->user_data); + + g_free(evt); + } + + g_slist_free(attrib->events); + attrib->events = NULL; + + return TRUE; +} diff --git a/attrib/gattrib.h b/attrib/gattrib.h new file mode 100644 index 0000000..f73b741 --- /dev/null +++ b/attrib/gattrib.h @@ -0,0 +1,78 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2010 Nokia Corporation + * Copyright (C) 2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ +#ifndef __GATTRIB_H +#define __GATTRIB_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define GATTRIB_ALL_EVENTS 0xFF +#define GATTRIB_ALL_REQS 0xFE + +struct _GAttrib; +typedef struct _GAttrib GAttrib; + +typedef void (*GAttribResultFunc) (guint8 status, const guint8 *pdu, + guint16 len, gpointer user_data); +typedef void (*GAttribDisconnectFunc)(gpointer user_data); +typedef void (*GAttribDebugFunc)(const char *str, gpointer user_data); +typedef void (*GAttribNotifyFunc)(const guint8 *pdu, guint16 len, + gpointer user_data); + +GAttrib *g_attrib_new(GIOChannel *io); +GAttrib *g_attrib_ref(GAttrib *attrib); +void g_attrib_unref(GAttrib *attrib); + +GIOChannel *g_attrib_get_channel(GAttrib *attrib); + +gboolean g_attrib_set_destroy_function(GAttrib *attrib, + GDestroyNotify destroy, gpointer user_data); + +guint g_attrib_send(GAttrib *attrib, guint id, guint8 opcode, + const guint8 *pdu, guint16 len, GAttribResultFunc func, + gpointer user_data, GDestroyNotify notify); + +gboolean g_attrib_cancel(GAttrib *attrib, guint id); +gboolean g_attrib_cancel_all(GAttrib *attrib); + +gboolean g_attrib_set_debug(GAttrib *attrib, + GAttribDebugFunc func, gpointer user_data); + +guint g_attrib_register(GAttrib *attrib, guint8 opcode, + GAttribNotifyFunc func, gpointer user_data, + GDestroyNotify notify); + +gboolean g_attrib_is_encrypted(GAttrib *attrib); + +uint8_t *g_attrib_get_buffer(GAttrib *attrib, int *len); +gboolean g_attrib_set_mtu(GAttrib *attrib, int mtu); + +gboolean g_attrib_unregister(GAttrib *attrib, guint id); +gboolean g_attrib_unregister_all(GAttrib *attrib); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/attrib/gatttool.c b/attrib/gatttool.c new file mode 100644 index 0000000..8a43ec1 --- /dev/null +++ b/attrib/gatttool.c @@ -0,0 +1,628 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2010 Nokia Corporation + * Copyright (C) 2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "att.h" +#include "btio.h" +#include "gattrib.h" +#include "gatt.h" +#include "gatttool.h" + +static gchar *opt_src = NULL; +static gchar *opt_dst = NULL; +static gchar *opt_dst_type = NULL; +static gchar *opt_value = NULL; +static gchar *opt_sec_level = NULL; +static bt_uuid_t *opt_uuid = NULL; +static int opt_start = 0x0001; +static int opt_end = 0xffff; +static int opt_handle = -1; +static int opt_mtu = 0; +static int opt_psm = 0; +static int opt_offset = 0; +static gboolean opt_primary = FALSE; +static gboolean opt_characteristics = FALSE; +static gboolean opt_char_read = FALSE; +static gboolean opt_listen = FALSE; +static gboolean opt_char_desc = FALSE; +static gboolean opt_char_write = FALSE; +static gboolean opt_char_write_req = FALSE; +static gboolean opt_interactive = FALSE; +static GMainLoop *event_loop; +static gboolean got_error = FALSE; +static GSourceFunc operation; + +struct characteristic_data { + GAttrib *attrib; + uint16_t start; + uint16_t end; +}; + +static void events_handler(const uint8_t *pdu, uint16_t len, gpointer user_data) +{ + GAttrib *attrib = user_data; + uint8_t opdu[ATT_MAX_MTU]; + uint16_t handle, i, olen = 0; + + handle = att_get_u16(&pdu[1]); + + switch (pdu[0]) { + case ATT_OP_HANDLE_NOTIFY: + g_print("Notification handle = 0x%04x value: ", handle); + break; + case ATT_OP_HANDLE_IND: + g_print("Indication handle = 0x%04x value: ", handle); + break; + default: + g_print("Invalid opcode\n"); + return; + } + + for (i = 3; i < len; i++) + g_print("%02x ", pdu[i]); + + g_print("\n"); + + if (pdu[0] == ATT_OP_HANDLE_NOTIFY) + return; + + olen = enc_confirmation(opdu, sizeof(opdu)); + + if (olen > 0) + g_attrib_send(attrib, 0, opdu[0], opdu, olen, NULL, NULL, NULL); +} + +static gboolean listen_start(gpointer user_data) +{ + GAttrib *attrib = user_data; + + g_attrib_register(attrib, ATT_OP_HANDLE_NOTIFY, events_handler, + attrib, NULL); + g_attrib_register(attrib, ATT_OP_HANDLE_IND, events_handler, + attrib, NULL); + + return FALSE; +} + +static void connect_cb(GIOChannel *io, GError *err, gpointer user_data) +{ + GAttrib *attrib; + + if (err) { + g_printerr("%s\n", err->message); + got_error = TRUE; + g_main_loop_quit(event_loop); + } + + attrib = g_attrib_new(io); + + if (opt_listen) + g_idle_add(listen_start, attrib); + + operation(attrib); +} + +static void primary_all_cb(GSList *services, guint8 status, gpointer user_data) +{ + GSList *l; + + if (status) { + g_printerr("Discover all primary services failed: %s\n", + att_ecode2str(status)); + goto done; + } + + for (l = services; l; l = l->next) { + struct gatt_primary *prim = l->data; + g_print("attr handle = 0x%04x, end grp handle = 0x%04x " + "uuid: %s\n", prim->range.start, prim->range.end, prim->uuid); + } + +done: + g_main_loop_quit(event_loop); +} + +static void primary_by_uuid_cb(GSList *ranges, guint8 status, + gpointer user_data) +{ + GSList *l; + + if (status != 0) { + g_printerr("Discover primary services by UUID failed: %s\n", + att_ecode2str(status)); + goto done; + } + + for (l = ranges; l; l = l->next) { + struct att_range *range = l->data; + g_print("Starting handle: %04x Ending handle: %04x\n", + range->start, range->end); + } + +done: + g_main_loop_quit(event_loop); +} + +static gboolean primary(gpointer user_data) +{ + GAttrib *attrib = user_data; + + if (opt_uuid) + gatt_discover_primary(attrib, opt_uuid, primary_by_uuid_cb, + NULL); + else + gatt_discover_primary(attrib, NULL, primary_all_cb, NULL); + + return FALSE; +} + +static void char_discovered_cb(GSList *characteristics, guint8 status, + gpointer user_data) +{ + GSList *l; + + if (status) { + g_printerr("Discover all characteristics failed: %s\n", + att_ecode2str(status)); + goto done; + } + + for (l = characteristics; l; l = l->next) { + struct gatt_char *chars = l->data; + + g_print("handle = 0x%04x, char properties = 0x%02x, char value " + "handle = 0x%04x, uuid = %s\n", chars->handle, + chars->properties, chars->value_handle, chars->uuid); + } + +done: + g_main_loop_quit(event_loop); +} + +static gboolean characteristics(gpointer user_data) +{ + GAttrib *attrib = user_data; + + gatt_discover_char(attrib, opt_start, opt_end, opt_uuid, + char_discovered_cb, NULL); + + return FALSE; +} + +static void char_read_cb(guint8 status, const guint8 *pdu, guint16 plen, + gpointer user_data) +{ + uint8_t value[ATT_MAX_MTU]; + int i, vlen; + + if (status != 0) { + g_printerr("Characteristic value/descriptor read failed: %s\n", + att_ecode2str(status)); + goto done; + } + if (!dec_read_resp(pdu, plen, value, &vlen)) { + g_printerr("Protocol error\n"); + goto done; + } + g_print("Characteristic value/descriptor: "); + for (i = 0; i < vlen; i++) + g_print("%02x ", value[i]); + g_print("\n"); + +done: + if (opt_listen == FALSE) + g_main_loop_quit(event_loop); +} + +static void char_read_by_uuid_cb(guint8 status, const guint8 *pdu, + guint16 plen, gpointer user_data) +{ + struct characteristic_data *char_data = user_data; + struct att_data_list *list; + int i; + + if (status == ATT_ECODE_ATTR_NOT_FOUND && + char_data->start != opt_start) + goto done; + + if (status != 0) { + g_printerr("Read characteristics by UUID failed: %s\n", + att_ecode2str(status)); + goto done; + } + + list = dec_read_by_type_resp(pdu, plen); + if (list == NULL) + goto done; + + for (i = 0; i < list->num; i++) { + uint8_t *value = list->data[i]; + int j; + + char_data->start = att_get_u16(value) + 1; + + g_print("handle: 0x%04x \t value: ", att_get_u16(value)); + value += 2; + for (j = 0; j < list->len - 2; j++, value++) + g_print("%02x ", *value); + g_print("\n"); + } + + att_data_list_free(list); + +done: + g_free(char_data); + g_main_loop_quit(event_loop); +} + +static gboolean characteristics_read(gpointer user_data) +{ + GAttrib *attrib = user_data; + + if (opt_uuid != NULL) { + struct characteristic_data *char_data; + + char_data = g_new(struct characteristic_data, 1); + char_data->attrib = attrib; + char_data->start = opt_start; + char_data->end = opt_end; + + gatt_read_char_by_uuid(attrib, opt_start, opt_end, opt_uuid, + char_read_by_uuid_cb, char_data); + + return FALSE; + } + + if (opt_handle <= 0) { + g_printerr("A valid handle is required\n"); + g_main_loop_quit(event_loop); + return FALSE; + } + + gatt_read_char(attrib, opt_handle, opt_offset, char_read_cb, attrib); + + return FALSE; +} + +static void mainloop_quit(gpointer user_data) +{ + uint8_t *value = user_data; + + g_free(value); + g_main_loop_quit(event_loop); +} + +static gboolean characteristics_write(gpointer user_data) +{ + GAttrib *attrib = user_data; + uint8_t *value; + size_t len; + + if (opt_handle <= 0) { + g_printerr("A valid handle is required\n"); + goto error; + } + + if (opt_value == NULL || opt_value[0] == '\0') { + g_printerr("A value is required\n"); + goto error; + } + + len = gatt_attr_data_from_string(opt_value, &value); + if (len == 0) { + g_printerr("Invalid value\n"); + goto error; + } + + gatt_write_cmd(attrib, opt_handle, value, len, mainloop_quit, value); + + return FALSE; + +error: + g_main_loop_quit(event_loop); + return FALSE; +} + +static void char_write_req_cb(guint8 status, const guint8 *pdu, guint16 plen, + gpointer user_data) +{ + if (status != 0) { + g_printerr("Characteristic Write Request failed: " + "%s\n", att_ecode2str(status)); + goto done; + } + + if (!dec_write_resp(pdu, plen)) { + g_printerr("Protocol error\n"); + goto done; + } + + g_print("Characteristic value was written successfully\n"); + +done: + if (opt_listen == FALSE) + g_main_loop_quit(event_loop); +} + +static gboolean characteristics_write_req(gpointer user_data) +{ + GAttrib *attrib = user_data; + uint8_t *value; + size_t len; + + if (opt_handle <= 0) { + g_printerr("A valid handle is required\n"); + goto error; + } + + if (opt_value == NULL || opt_value[0] == '\0') { + g_printerr("A value is required\n"); + goto error; + } + + len = gatt_attr_data_from_string(opt_value, &value); + if (len == 0) { + g_printerr("Invalid value\n"); + goto error; + } + + gatt_write_char(attrib, opt_handle, value, len, char_write_req_cb, + NULL); + + return FALSE; + +error: + g_main_loop_quit(event_loop); + return FALSE; +} + +static void char_desc_cb(guint8 status, const guint8 *pdu, guint16 plen, + gpointer user_data) +{ + struct att_data_list *list; + guint8 format; + int i; + + if (status != 0) { + g_printerr("Discover all characteristic descriptors failed: " + "%s\n", att_ecode2str(status)); + goto done; + } + + list = dec_find_info_resp(pdu, plen, &format); + if (list == NULL) + goto done; + + for (i = 0; i < list->num; i++) { + char uuidstr[MAX_LEN_UUID_STR]; + uint16_t handle; + uint8_t *value; + bt_uuid_t uuid; + + value = list->data[i]; + handle = att_get_u16(value); + + if (format == 0x01) + uuid = att_get_uuid16(&value[2]); + else + uuid = att_get_uuid128(&value[2]); + + bt_uuid_to_string(&uuid, uuidstr, MAX_LEN_UUID_STR); + g_print("handle = 0x%04x, uuid = %s\n", handle, uuidstr); + } + + att_data_list_free(list); + +done: + if (opt_listen == FALSE) + g_main_loop_quit(event_loop); +} + +static gboolean characteristics_desc(gpointer user_data) +{ + GAttrib *attrib = user_data; + + gatt_find_info(attrib, opt_start, opt_end, char_desc_cb, NULL); + + return FALSE; +} + +static gboolean parse_uuid(const char *key, const char *value, + gpointer user_data, GError **error) +{ + if (!value) + return FALSE; + + opt_uuid = g_try_malloc(sizeof(bt_uuid_t)); + if (opt_uuid == NULL) + return FALSE; + + if (bt_string_to_uuid(opt_uuid, value) < 0) + return FALSE; + + return TRUE; +} + +static GOptionEntry primary_char_options[] = { + { "start", 's' , 0, G_OPTION_ARG_INT, &opt_start, + "Starting handle(optional)", "0x0001" }, + { "end", 'e' , 0, G_OPTION_ARG_INT, &opt_end, + "Ending handle(optional)", "0xffff" }, + { "uuid", 'u', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, + parse_uuid, "UUID16 or UUID128(optional)", "0x1801"}, + { NULL }, +}; + +static GOptionEntry char_rw_options[] = { + { "handle", 'a' , 0, G_OPTION_ARG_INT, &opt_handle, + "Read/Write characteristic by handle(required)", "0x0001" }, + { "value", 'n' , 0, G_OPTION_ARG_STRING, &opt_value, + "Write characteristic value (required for write operation)", + "0x0001" }, + { "offset", 'o', 0, G_OPTION_ARG_INT, &opt_offset, + "Offset to long read characteristic by handle", "N"}, + {NULL}, +}; + +static GOptionEntry gatt_options[] = { + { "primary", 0, 0, G_OPTION_ARG_NONE, &opt_primary, + "Primary Service Discovery", NULL }, + { "characteristics", 0, 0, G_OPTION_ARG_NONE, &opt_characteristics, + "Characteristics Discovery", NULL }, + { "char-read", 0, 0, G_OPTION_ARG_NONE, &opt_char_read, + "Characteristics Value/Descriptor Read", NULL }, + { "char-write", 0, 0, G_OPTION_ARG_NONE, &opt_char_write, + "Characteristics Value Write Without Response (Write Command)", + NULL }, + { "char-write-req", 0, 0, G_OPTION_ARG_NONE, &opt_char_write_req, + "Characteristics Value Write (Write Request)", NULL }, + { "char-desc", 0, 0, G_OPTION_ARG_NONE, &opt_char_desc, + "Characteristics Descriptor Discovery", NULL }, + { "listen", 0, 0, G_OPTION_ARG_NONE, &opt_listen, + "Listen for notifications and indications", NULL }, + { "interactive", 'I', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE, + &opt_interactive, "Use interactive mode", NULL }, + { NULL }, +}; + +static GOptionEntry options[] = { + { "adapter", 'i', 0, G_OPTION_ARG_STRING, &opt_src, + "Specify local adapter interface", "hciX" }, + { "device", 'b', 0, G_OPTION_ARG_STRING, &opt_dst, + "Specify remote Bluetooth address", "MAC" }, + { "addr-type", 't', 0, G_OPTION_ARG_STRING, &opt_dst_type, + "Set LE address type. Default: public", "[public | random]"}, + { "mtu", 'm', 0, G_OPTION_ARG_INT, &opt_mtu, + "Specify the MTU size", "MTU" }, + { "psm", 'p', 0, G_OPTION_ARG_INT, &opt_psm, + "Specify the PSM for GATT/ATT over BR/EDR", "PSM" }, + { "sec-level", 'l', 0, G_OPTION_ARG_STRING, &opt_sec_level, + "Set security level. Default: low", "[low | medium | high]"}, + { NULL }, +}; + +int main(int argc, char *argv[]) +{ + GOptionContext *context; + GOptionGroup *gatt_group, *params_group, *char_rw_group; + GError *gerr = NULL; + GIOChannel *chan; + + opt_dst_type = g_strdup("public"); + opt_sec_level = g_strdup("low"); + + context = g_option_context_new(NULL); + g_option_context_add_main_entries(context, options, NULL); + + /* GATT commands */ + gatt_group = g_option_group_new("gatt", "GATT commands", + "Show all GATT commands", NULL, NULL); + g_option_context_add_group(context, gatt_group); + g_option_group_add_entries(gatt_group, gatt_options); + + /* Primary Services and Characteristics arguments */ + params_group = g_option_group_new("params", + "Primary Services/Characteristics arguments", + "Show all Primary Services/Characteristics arguments", + NULL, NULL); + g_option_context_add_group(context, params_group); + g_option_group_add_entries(params_group, primary_char_options); + + /* Characteristics value/descriptor read/write arguments */ + char_rw_group = g_option_group_new("char-read-write", + "Characteristics Value/Descriptor Read/Write arguments", + "Show all Characteristics Value/Descriptor Read/Write " + "arguments", + NULL, NULL); + g_option_context_add_group(context, char_rw_group); + g_option_group_add_entries(char_rw_group, char_rw_options); + + if (g_option_context_parse(context, &argc, &argv, &gerr) == FALSE) { + g_printerr("%s\n", gerr->message); + g_error_free(gerr); + } + + if (opt_interactive) { + interactive(opt_src, opt_dst, opt_dst_type, opt_psm); + goto done; + } + + if (opt_primary) + operation = primary; + else if (opt_characteristics) + operation = characteristics; + else if (opt_char_read) + operation = characteristics_read; + else if (opt_char_write) + operation = characteristics_write; + else if (opt_char_write_req) + operation = characteristics_write_req; + else if (opt_char_desc) + operation = characteristics_desc; + else { + gchar *help = g_option_context_get_help(context, TRUE, NULL); + g_print("%s\n", help); + g_free(help); + got_error = TRUE; + goto done; + } + + chan = gatt_connect(opt_src, opt_dst, opt_dst_type, opt_sec_level, + opt_psm, opt_mtu, connect_cb); + if (chan == NULL) { + got_error = TRUE; + goto done; + } + + event_loop = g_main_loop_new(NULL, FALSE); + + g_main_loop_run(event_loop); + + g_main_loop_unref(event_loop); + +done: + g_option_context_free(context); + g_free(opt_src); + g_free(opt_dst); + g_free(opt_uuid); + g_free(opt_sec_level); + + if (got_error) + exit(EXIT_FAILURE); + else + exit(EXIT_SUCCESS); +} diff --git a/attrib/gatttool.h b/attrib/gatttool.h new file mode 100644 index 0000000..a38339b --- /dev/null +++ b/attrib/gatttool.h @@ -0,0 +1,29 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2011 Nokia Corporation + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +int interactive(const gchar *src, const gchar *dst, const gchar *dst_type, + gboolean le); +GIOChannel *gatt_connect(const gchar *src, const gchar *dst, + const gchar *dst_type, const gchar *sec_level, + int psm, int mtu, BtIOConnect connect_cb); +size_t gatt_attr_data_from_string(const char *str, uint8_t **data); diff --git a/attrib/interactive.c b/attrib/interactive.c new file mode 100644 index 0000000..0a01cdf --- /dev/null +++ b/attrib/interactive.c @@ -0,0 +1,890 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2011 Nokia Corporation + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "att.h" +#include "btio.h" +#include "gattrib.h" +#include "gatt.h" +#include "gatttool.h" + +static GIOChannel *iochannel = NULL; +static GAttrib *attrib = NULL; +static GMainLoop *event_loop; +static GString *prompt; + +static gchar *opt_src = NULL; +static gchar *opt_dst = NULL; +static gchar *opt_dst_type = NULL; +static gchar *opt_sec_level = NULL; +static int opt_psm = 0; +static int opt_mtu = 0; + +struct characteristic_data { + uint16_t orig_start; + uint16_t start; + uint16_t end; + bt_uuid_t uuid; +}; + +static void cmd_help(int argcp, char **argvp); + +enum state { + STATE_DISCONNECTED, + STATE_CONNECTING, + STATE_CONNECTED +} conn_state; + +static char *get_prompt(void) +{ + if (conn_state == STATE_CONNECTING) { + g_string_assign(prompt, "Connecting... "); + return prompt->str; + } + + if (conn_state == STATE_CONNECTED) + g_string_assign(prompt, "[CON]"); + else + g_string_assign(prompt, "[ ]"); + + if (opt_dst) + g_string_append_printf(prompt, "[%17s]", opt_dst); + else + g_string_append_printf(prompt, "[%17s]", ""); + + if (opt_psm) + g_string_append(prompt, "[BR]"); + else + g_string_append(prompt, "[LE]"); + + g_string_append(prompt, "> "); + + return prompt->str; +} + + +static void set_state(enum state st) +{ + conn_state = st; + rl_set_prompt(get_prompt()); + rl_redisplay(); +} + +static void events_handler(const uint8_t *pdu, uint16_t len, gpointer user_data) +{ + uint8_t opdu[ATT_MAX_MTU]; + uint16_t handle, i, olen; + + handle = att_get_u16(&pdu[1]); + + printf("\n"); + switch (pdu[0]) { + case ATT_OP_HANDLE_NOTIFY: + printf("Notification handle = 0x%04x value: ", handle); + break; + case ATT_OP_HANDLE_IND: + printf("Indication handle = 0x%04x value: ", handle); + break; + default: + printf("Invalid opcode\n"); + return; + } + + for (i = 3; i < len; i++) + printf("%02x ", pdu[i]); + + printf("\n"); + rl_forced_update_display(); + + if (pdu[0] == ATT_OP_HANDLE_NOTIFY) + return; + + olen = enc_confirmation(opdu, sizeof(opdu)); + + if (olen > 0) + g_attrib_send(attrib, 0, opdu[0], opdu, olen, NULL, NULL, NULL); +} + +static void connect_cb(GIOChannel *io, GError *err, gpointer user_data) +{ + if (err) { + printf("connect error: %s\n", err->message); + set_state(STATE_DISCONNECTED); + return; + } + + attrib = g_attrib_new(iochannel); + g_attrib_register(attrib, ATT_OP_HANDLE_NOTIFY, events_handler, + attrib, NULL); + g_attrib_register(attrib, ATT_OP_HANDLE_IND, events_handler, + attrib, NULL); + set_state(STATE_CONNECTED); +} + +static void disconnect_io() +{ + if (conn_state == STATE_DISCONNECTED) + return; + + g_attrib_unref(attrib); + attrib = NULL; + opt_mtu = 0; + + g_io_channel_shutdown(iochannel, FALSE, NULL); + g_io_channel_unref(iochannel); + iochannel = NULL; + + set_state(STATE_DISCONNECTED); +} + +static void primary_all_cb(GSList *services, guint8 status, gpointer user_data) +{ + GSList *l; + + if (status) { + printf("Discover all primary services failed: %s\n", + att_ecode2str(status)); + return; + } + + printf("\n"); + for (l = services; l; l = l->next) { + struct gatt_primary *prim = l->data; + printf("attr handle: 0x%04x, end grp handle: 0x%04x " + "uuid: %s\n", prim->range.start, prim->range.end, prim->uuid); + } + + rl_forced_update_display(); +} + +static void primary_by_uuid_cb(GSList *ranges, guint8 status, + gpointer user_data) +{ + GSList *l; + + if (status) { + printf("Discover primary services by UUID failed: %s\n", + att_ecode2str(status)); + return; + } + + printf("\n"); + for (l = ranges; l; l = l->next) { + struct att_range *range = l->data; + g_print("Starting handle: 0x%04x Ending handle: 0x%04x\n", + range->start, range->end); + } + + rl_forced_update_display(); +} + +static void char_cb(GSList *characteristics, guint8 status, gpointer user_data) +{ + GSList *l; + + if (status) { + printf("Discover all characteristics failed: %s\n", + att_ecode2str(status)); + return; + } + + printf("\n"); + for (l = characteristics; l; l = l->next) { + struct gatt_char *chars = l->data; + + printf("handle: 0x%04x, char properties: 0x%02x, char value " + "handle: 0x%04x, uuid: %s\n", chars->handle, + chars->properties, chars->value_handle, + chars->uuid); + } + + rl_forced_update_display(); +} + +static void char_desc_cb(guint8 status, const guint8 *pdu, guint16 plen, + gpointer user_data) +{ + struct att_data_list *list; + guint8 format; + int i; + + if (status != 0) { + printf("Discover all characteristic descriptors failed: " + "%s\n", att_ecode2str(status)); + return; + } + + list = dec_find_info_resp(pdu, plen, &format); + if (list == NULL) + return; + + printf("\n"); + for (i = 0; i < list->num; i++) { + char uuidstr[MAX_LEN_UUID_STR]; + uint16_t handle; + uint8_t *value; + bt_uuid_t uuid; + + value = list->data[i]; + handle = att_get_u16(value); + + if (format == 0x01) + uuid = att_get_uuid16(&value[2]); + else + uuid = att_get_uuid128(&value[2]); + + bt_uuid_to_string(&uuid, uuidstr, MAX_LEN_UUID_STR); + printf("handle: 0x%04x, uuid: %s\n", handle, uuidstr); + } + + att_data_list_free(list); + + rl_forced_update_display(); +} + +static void char_read_cb(guint8 status, const guint8 *pdu, guint16 plen, + gpointer user_data) +{ + uint8_t value[ATT_MAX_MTU]; + int i, vlen; + + if (status != 0) { + printf("Characteristic value/descriptor read failed: %s\n", + att_ecode2str(status)); + return; + } + + if (!dec_read_resp(pdu, plen, value, &vlen)) { + printf("Protocol error\n"); + return; + } + + printf("\nCharacteristic value/descriptor: "); + for (i = 0; i < vlen; i++) + printf("%02x ", value[i]); + printf("\n"); + + rl_forced_update_display(); +} + +static void char_read_by_uuid_cb(guint8 status, const guint8 *pdu, + guint16 plen, gpointer user_data) +{ + struct characteristic_data *char_data = user_data; + struct att_data_list *list; + int i; + + if (status == ATT_ECODE_ATTR_NOT_FOUND && + char_data->start != char_data->orig_start) + goto done; + + if (status != 0) { + printf("Read characteristics by UUID failed: %s\n", + att_ecode2str(status)); + goto done; + } + + list = dec_read_by_type_resp(pdu, plen); + if (list == NULL) + goto done; + + for (i = 0; i < list->num; i++) { + uint8_t *value = list->data[i]; + int j; + + char_data->start = att_get_u16(value) + 1; + + printf("\nhandle: 0x%04x \t value: ", att_get_u16(value)); + value += 2; + for (j = 0; j < list->len - 2; j++, value++) + printf("%02x ", *value); + printf("\n"); + } + + att_data_list_free(list); + + rl_forced_update_display(); + +done: + g_free(char_data); +} + +static void cmd_exit(int argcp, char **argvp) +{ + rl_callback_handler_remove(); + g_main_loop_quit(event_loop); +} + +static gboolean channel_watcher(GIOChannel *chan, GIOCondition cond, + gpointer user_data) +{ + disconnect_io(); + + return FALSE; +} + +static void cmd_connect(int argcp, char **argvp) +{ + if (conn_state != STATE_DISCONNECTED) + return; + + if (argcp > 1) { + g_free(opt_dst); + opt_dst = g_strdup(argvp[1]); + + g_free(opt_dst_type); + if (argcp > 2) + opt_dst_type = g_strdup(argvp[2]); + else + opt_dst_type = g_strdup("public"); + } + + if (opt_dst == NULL) { + printf("Remote Bluetooth address required\n"); + return; + } + + set_state(STATE_CONNECTING); + iochannel = gatt_connect(opt_src, opt_dst, opt_dst_type, opt_sec_level, + opt_psm, opt_mtu, connect_cb); + if (iochannel == NULL) + set_state(STATE_DISCONNECTED); + else + g_io_add_watch(iochannel, G_IO_HUP, channel_watcher, NULL); +} + +static void cmd_disconnect(int argcp, char **argvp) +{ + disconnect_io(); +} + +static void cmd_primary(int argcp, char **argvp) +{ + bt_uuid_t uuid; + + if (conn_state != STATE_CONNECTED) { + printf("Command failed: disconnected\n"); + return; + } + + if (argcp == 1) { + gatt_discover_primary(attrib, NULL, primary_all_cb, NULL); + return; + } + + if (bt_string_to_uuid(&uuid, argvp[1]) < 0) { + printf("Invalid UUID\n"); + return; + } + + gatt_discover_primary(attrib, &uuid, primary_by_uuid_cb, NULL); +} + +static int strtohandle(const char *src) +{ + char *e; + int dst; + + errno = 0; + dst = strtoll(src, &e, 16); + if (errno != 0 || *e != '\0') + return -EINVAL; + + return dst; +} + +static void cmd_char(int argcp, char **argvp) +{ + int start = 0x0001; + int end = 0xffff; + + if (conn_state != STATE_CONNECTED) { + printf("Command failed: disconnected\n"); + return; + } + + if (argcp > 1) { + start = strtohandle(argvp[1]); + if (start < 0) { + printf("Invalid start handle: %s\n", argvp[1]); + return; + } + } + + if (argcp > 2) { + end = strtohandle(argvp[2]); + if (end < 0) { + printf("Invalid end handle: %s\n", argvp[2]); + return; + } + } + + if (argcp > 3) { + bt_uuid_t uuid; + + if (bt_string_to_uuid(&uuid, argvp[3]) < 0) { + printf("Invalid UUID\n"); + return; + } + + gatt_discover_char(attrib, start, end, &uuid, char_cb, NULL); + return; + } + + gatt_discover_char(attrib, start, end, NULL, char_cb, NULL); +} + +static void cmd_char_desc(int argcp, char **argvp) +{ + int start = 0x0001; + int end = 0xffff; + + if (conn_state != STATE_CONNECTED) { + printf("Command failed: disconnected\n"); + return; + } + + if (argcp > 1) { + start = strtohandle(argvp[1]); + if (start < 0) { + printf("Invalid start handle: %s\n", argvp[1]); + return; + } + } + + if (argcp > 2) { + end = strtohandle(argvp[2]); + if (end < 0) { + printf("Invalid end handle: %s\n", argvp[2]); + return; + } + } + + gatt_find_info(attrib, start, end, char_desc_cb, NULL); +} + +static void cmd_read_hnd(int argcp, char **argvp) +{ + int handle; + int offset = 0; + + if (conn_state != STATE_CONNECTED) { + printf("Command failed: disconnected\n"); + return; + } + + if (argcp < 2) { + printf("Missing argument: handle\n"); + return; + } + + handle = strtohandle(argvp[1]); + if (handle < 0) { + printf("Invalid handle: %s\n", argvp[1]); + return; + } + + if (argcp > 2) { + char *e; + + errno = 0; + offset = strtol(argvp[2], &e, 0); + if (errno != 0 || *e != '\0') { + printf("Invalid offset: %s\n", argvp[2]); + return; + } + } + + gatt_read_char(attrib, handle, offset, char_read_cb, attrib); +} + +static void cmd_read_uuid(int argcp, char **argvp) +{ + struct characteristic_data *char_data; + int start = 0x0001; + int end = 0xffff; + bt_uuid_t uuid; + + if (conn_state != STATE_CONNECTED) { + printf("Command failed: disconnected\n"); + return; + } + + if (argcp < 2) { + printf("Missing argument: UUID\n"); + return; + } + + if (bt_string_to_uuid(&uuid, argvp[1]) < 0) { + printf("Invalid UUID\n"); + return; + } + + if (argcp > 2) { + start = strtohandle(argvp[2]); + if (start < 0) { + printf("Invalid start handle: %s\n", argvp[1]); + return; + } + } + + if (argcp > 3) { + end = strtohandle(argvp[3]); + if (end < 0) { + printf("Invalid end handle: %s\n", argvp[2]); + return; + } + } + + char_data = g_new(struct characteristic_data, 1); + char_data->orig_start = start; + char_data->start = start; + char_data->end = end; + char_data->uuid = uuid; + + gatt_read_char_by_uuid(attrib, start, end, &char_data->uuid, + char_read_by_uuid_cb, char_data); +} + +static void char_write_req_cb(guint8 status, const guint8 *pdu, guint16 plen, + gpointer user_data) +{ + if (status != 0) { + printf("Characteristic Write Request failed: " + "%s\n", att_ecode2str(status)); + return; + } + + if (!dec_write_resp(pdu, plen)) { + printf("Protocol error\n"); + return; + } + + printf("Characteristic value was written successfully\n"); +} + +static void cmd_char_write(int argcp, char **argvp) +{ + uint8_t *value; + size_t plen; + int handle; + + if (conn_state != STATE_CONNECTED) { + printf("Command failed: disconnected\n"); + return; + } + + if (argcp < 3) { + printf("Usage: %s \n", argvp[0]); + return; + } + + handle = strtohandle(argvp[1]); + if (handle <= 0) { + printf("A valid handle is required\n"); + return; + } + + plen = gatt_attr_data_from_string(argvp[2], &value); + if (plen == 0) { + g_printerr("Invalid value\n"); + return; + } + + if (g_strcmp0("char-write-req", argvp[0]) == 0) + gatt_write_char(attrib, handle, value, plen, + char_write_req_cb, NULL); + else + gatt_write_char(attrib, handle, value, plen, NULL, NULL); + + g_free(value); +} + +static void cmd_sec_level(int argcp, char **argvp) +{ + GError *gerr = NULL; + BtIOSecLevel sec_level; + + if (argcp < 2) { + printf("sec-level: %s\n", opt_sec_level); + return; + } + + if (strcasecmp(argvp[1], "medium") == 0) + sec_level = BT_IO_SEC_MEDIUM; + else if (strcasecmp(argvp[1], "high") == 0) + sec_level = BT_IO_SEC_HIGH; + else if (strcasecmp(argvp[1], "low") == 0) + sec_level = BT_IO_SEC_LOW; + else { + printf("Allowed values: low | medium | high\n"); + return; + } + + g_free(opt_sec_level); + opt_sec_level = g_strdup(argvp[1]); + + if (conn_state != STATE_CONNECTED) + return; + + if (opt_psm) { + printf("It must be reconnected to this change take effect\n"); + return; + } + + bt_io_set(iochannel, BT_IO_L2CAP, &gerr, + BT_IO_OPT_SEC_LEVEL, sec_level, + BT_IO_OPT_INVALID); + + if (gerr) { + printf("Error: %s\n", gerr->message); + g_error_free(gerr); + } +} + +static void exchange_mtu_cb(guint8 status, const guint8 *pdu, guint16 plen, + gpointer user_data) +{ + uint16_t mtu; + + if (status != 0) { + printf("Exchange MTU Request failed: %s\n", + att_ecode2str(status)); + return; + } + + if (!dec_mtu_resp(pdu, plen, &mtu)) { + printf("Protocol error\n"); + return; + } + + mtu = MIN(mtu, opt_mtu); + /* Set new value for MTU in client */ + if (g_attrib_set_mtu(attrib, mtu)) + printf("MTU was exchanged successfully: %d\n", mtu); + else + printf("Error exchanging MTU\n"); +} + +static void cmd_mtu(int argcp, char **argvp) +{ + if (conn_state != STATE_CONNECTED) { + printf("Command failed: not connected.\n"); + return; + } + + if (opt_psm) { + printf("Command failed: operation is only available for LE" + " transport.\n"); + return; + } + + if (argcp < 2) { + printf("Usage: mtu \n"); + return; + } + + if (opt_mtu) { + printf("Command failed: MTU exchange can only occur once per" + " connection.\n"); + return; + } + + errno = 0; + opt_mtu = strtoll(argvp[1], NULL, 0); + if (errno != 0 || opt_mtu < ATT_DEFAULT_LE_MTU) { + printf("Invalid value. Minimum MTU size is %d\n", + ATT_DEFAULT_LE_MTU); + return; + } + + gatt_exchange_mtu(attrib, opt_mtu, exchange_mtu_cb, NULL); +} + +static struct { + const char *cmd; + void (*func)(int argcp, char **argvp); + const char *params; + const char *desc; +} commands[] = { + { "help", cmd_help, "", + "Show this help"}, + { "exit", cmd_exit, "", + "Exit interactive mode" }, + { "quit", cmd_exit, "", + "Exit interactive mode" }, + { "connect", cmd_connect, "[address [address type]]", + "Connect to a remote device" }, + { "disconnect", cmd_disconnect, "", + "Disconnect from a remote device" }, + { "primary", cmd_primary, "[UUID]", + "Primary Service Discovery" }, + { "characteristics", cmd_char, "[start hnd [end hnd [UUID]]]", + "Characteristics Discovery" }, + { "char-desc", cmd_char_desc, "[start hnd] [end hnd]", + "Characteristics Descriptor Discovery" }, + { "char-read-hnd", cmd_read_hnd, " [offset]", + "Characteristics Value/Descriptor Read by handle" }, + { "char-read-uuid", cmd_read_uuid, " [start hnd] [end hnd]", + "Characteristics Value/Descriptor Read by UUID" }, + { "char-write-req", cmd_char_write, " ", + "Characteristic Value Write (Write Request)" }, + { "char-write-cmd", cmd_char_write, " ", + "Characteristic Value Write (No response)" }, + { "sec-level", cmd_sec_level, "[low | medium | high]", + "Set security level. Default: low" }, + { "mtu", cmd_mtu, "", + "Exchange MTU for GATT/ATT" }, + { NULL, NULL, NULL} +}; + +static void cmd_help(int argcp, char **argvp) +{ + int i; + + for (i = 0; commands[i].cmd; i++) + printf("%-15s %-30s %s\n", commands[i].cmd, + commands[i].params, commands[i].desc); +} + +static void parse_line(char *line_read) +{ + gchar **argvp; + int argcp; + int i; + + if (line_read == NULL) { + printf("\n"); + cmd_exit(0, NULL); + return; + } + + line_read = g_strstrip(line_read); + + if (*line_read == '\0') + return; + + add_history(line_read); + + g_shell_parse_argv(line_read, &argcp, &argvp, NULL); + + for (i = 0; commands[i].cmd; i++) + if (strcasecmp(commands[i].cmd, argvp[0]) == 0) + break; + + if (commands[i].cmd) + commands[i].func(argcp, argvp); + else + printf("%s: command not found\n", argvp[0]); + + g_strfreev(argvp); +} + +static gboolean prompt_read(GIOChannel *chan, GIOCondition cond, + gpointer user_data) +{ + if (cond & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) { + g_io_channel_unref(chan); + return FALSE; + } + + rl_callback_read_char(); + + return TRUE; +} + +static char *completion_generator(const char *text, int state) +{ + static int index = 0, len = 0; + const char *cmd = NULL; + + if (state == 0) { + index = 0; + len = strlen(text); + } + + while ((cmd = commands[index].cmd) != NULL) { + index++; + if (strncmp(cmd, text, len) == 0) + return strdup(cmd); + } + + return NULL; +} + +static char **commands_completion(const char *text, int start, int end) +{ + if (start == 0) + return rl_completion_matches(text, &completion_generator); + else + return NULL; +} + +int interactive(const gchar *src, const gchar *dst, + const gchar *dst_type, int psm) +{ + GIOChannel *pchan; + gint events; + + opt_sec_level = g_strdup("low"); + + opt_src = g_strdup(src); + opt_dst = g_strdup(dst); + opt_dst_type = g_strdup(dst_type); + opt_psm = psm; + + prompt = g_string_new(NULL); + + event_loop = g_main_loop_new(NULL, FALSE); + + pchan = g_io_channel_unix_new(fileno(stdin)); + g_io_channel_set_close_on_unref(pchan, TRUE); + events = G_IO_IN | G_IO_ERR | G_IO_HUP | G_IO_NVAL; + g_io_add_watch(pchan, events, prompt_read, NULL); + + rl_attempted_completion_function = commands_completion; + rl_callback_handler_install(get_prompt(), parse_line); + + g_main_loop_run(event_loop); + + rl_callback_handler_remove(); + cmd_disconnect(0, NULL); + g_io_channel_unref(pchan); + g_main_loop_unref(event_loop); + g_string_free(prompt, TRUE); + + g_free(opt_src); + g_free(opt_dst); + g_free(opt_sec_level); + + return 0; +} diff --git a/attrib/utils.c b/attrib/utils.c new file mode 100644 index 0000000..d856fe2 --- /dev/null +++ b/attrib/utils.c @@ -0,0 +1,121 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2011 Nokia Corporation + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include + +#include +#include +#include +#include +#include + +#include "att.h" +#include "gattrib.h" +#include "gatt.h" +#include "btio.h" +#include "gatttool.h" + +GIOChannel *gatt_connect(const gchar *src, const gchar *dst, + const gchar *dst_type, const gchar *sec_level, + int psm, int mtu, BtIOConnect connect_cb) +{ + GIOChannel *chan; + bdaddr_t sba, dba; + uint8_t dest_type; + GError *err = NULL; + BtIOSecLevel sec; + + /* Remote device */ + if (dst == NULL) { + g_printerr("Remote Bluetooth address required\n"); + return NULL; + } + str2ba(dst, &dba); + + /* Local adapter */ + if (src != NULL) { + if (!strncmp(src, "hci", 3)) + hci_devba(atoi(src + 3), &sba); + else + str2ba(src, &sba); + } else + bacpy(&sba, BDADDR_ANY); + + /* Not used for BR/EDR */ + if (strcmp(dst_type, "random") == 0) + dest_type = BDADDR_LE_RANDOM; + else + dest_type = BDADDR_LE_PUBLIC; + + if (strcmp(sec_level, "medium") == 0) + sec = BT_IO_SEC_MEDIUM; + else if (strcmp(sec_level, "high") == 0) + sec = BT_IO_SEC_HIGH; + else + sec = BT_IO_SEC_LOW; + + if (psm == 0) + chan = bt_io_connect(BT_IO_L2CAP, connect_cb, NULL, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &sba, + BT_IO_OPT_DEST_BDADDR, &dba, + BT_IO_OPT_DEST_TYPE, dest_type, + BT_IO_OPT_CID, ATT_CID, + BT_IO_OPT_SEC_LEVEL, sec, + BT_IO_OPT_INVALID); + else + chan = bt_io_connect(BT_IO_L2CAP, connect_cb, NULL, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &sba, + BT_IO_OPT_DEST_BDADDR, &dba, + BT_IO_OPT_PSM, psm, + BT_IO_OPT_IMTU, mtu, + BT_IO_OPT_SEC_LEVEL, sec, + BT_IO_OPT_INVALID); + + if (err) { + g_printerr("%s\n", err->message); + g_error_free(err); + return NULL; + } + + return chan; +} + +size_t gatt_attr_data_from_string(const char *str, uint8_t **data) +{ + char tmp[3]; + size_t size, i; + + size = strlen(str) / 2; + *data = g_try_malloc0(size); + if (*data == NULL) + return 0; + + tmp[2] = '\0'; + for (i = 0; i < size; i++) { + memcpy(tmp, str + (i * 2), 2); + (*data)[i] = (uint8_t) strtol(tmp, NULL, 16); + } + + return size; +} diff --git a/audio/a2dp-codecs.h b/audio/a2dp-codecs.h new file mode 100644 index 0000000..3dc31cb --- /dev/null +++ b/audio/a2dp-codecs.h @@ -0,0 +1,139 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define A2DP_CODEC_SBC 0x00 +#define A2DP_CODEC_MPEG12 0x01 +#define A2DP_CODEC_MPEG24 0x02 +#define A2DP_CODEC_ATRAC 0x03 +#define A2DP_CODEC_VENDOR 0xFF + +#define SBC_SAMPLING_FREQ_16000 (1 << 3) +#define SBC_SAMPLING_FREQ_32000 (1 << 2) +#define SBC_SAMPLING_FREQ_44100 (1 << 1) +#define SBC_SAMPLING_FREQ_48000 1 + +#define SBC_CHANNEL_MODE_MONO (1 << 3) +#define SBC_CHANNEL_MODE_DUAL_CHANNEL (1 << 2) +#define SBC_CHANNEL_MODE_STEREO (1 << 1) +#define SBC_CHANNEL_MODE_JOINT_STEREO 1 + +#define SBC_BLOCK_LENGTH_4 (1 << 3) +#define SBC_BLOCK_LENGTH_8 (1 << 2) +#define SBC_BLOCK_LENGTH_12 (1 << 1) +#define SBC_BLOCK_LENGTH_16 1 + +#define SBC_SUBBANDS_4 (1 << 1) +#define SBC_SUBBANDS_8 1 + +#define SBC_ALLOCATION_SNR (1 << 1) +#define SBC_ALLOCATION_LOUDNESS 1 + +#define MAX_BITPOOL 64 +#define MIN_BITPOOL 2 + +#define MPEG_CHANNEL_MODE_MONO (1 << 3) +#define MPEG_CHANNEL_MODE_DUAL_CHANNEL (1 << 2) +#define MPEG_CHANNEL_MODE_STEREO (1 << 1) +#define MPEG_CHANNEL_MODE_JOINT_STEREO 1 + +#define MPEG_LAYER_MP1 (1 << 2) +#define MPEG_LAYER_MP2 (1 << 1) +#define MPEG_LAYER_MP3 1 + +#define MPEG_SAMPLING_FREQ_16000 (1 << 5) +#define MPEG_SAMPLING_FREQ_22050 (1 << 4) +#define MPEG_SAMPLING_FREQ_24000 (1 << 3) +#define MPEG_SAMPLING_FREQ_32000 (1 << 2) +#define MPEG_SAMPLING_FREQ_44100 (1 << 1) +#define MPEG_SAMPLING_FREQ_48000 1 + +#define MPEG_BIT_RATE_VBR 0x8000 +#define MPEG_BIT_RATE_320000 0x4000 +#define MPEG_BIT_RATE_256000 0x2000 +#define MPEG_BIT_RATE_224000 0x1000 +#define MPEG_BIT_RATE_192000 0x0800 +#define MPEG_BIT_RATE_160000 0x0400 +#define MPEG_BIT_RATE_128000 0x0200 +#define MPEG_BIT_RATE_112000 0x0100 +#define MPEG_BIT_RATE_96000 0x0080 +#define MPEG_BIT_RATE_80000 0x0040 +#define MPEG_BIT_RATE_64000 0x0020 +#define MPEG_BIT_RATE_56000 0x0010 +#define MPEG_BIT_RATE_48000 0x0008 +#define MPEG_BIT_RATE_40000 0x0004 +#define MPEG_BIT_RATE_32000 0x0002 +#define MPEG_BIT_RATE_FREE 0x0001 + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +typedef struct { + uint8_t channel_mode:4; + uint8_t frequency:4; + uint8_t allocation_method:2; + uint8_t subbands:2; + uint8_t block_length:4; + uint8_t min_bitpool; + uint8_t max_bitpool; +} __attribute__ ((packed)) a2dp_sbc_t; + +typedef struct { + uint8_t channel_mode:4; + uint8_t crc:1; + uint8_t layer:3; + uint8_t frequency:6; + uint8_t mpf:1; + uint8_t rfa:1; + uint16_t bitrate; +} __attribute__ ((packed)) a2dp_mpeg_t; + +#elif __BYTE_ORDER == __BIG_ENDIAN + +typedef struct { + uint8_t frequency:4; + uint8_t channel_mode:4; + uint8_t block_length:4; + uint8_t subbands:2; + uint8_t allocation_method:2; + uint8_t min_bitpool; + uint8_t max_bitpool; +} __attribute__ ((packed)) a2dp_sbc_t; + +typedef struct { + uint8_t layer:3; + uint8_t crc:1; + uint8_t channel_mode:4; + uint8_t rfa:1; + uint8_t mpf:1; + uint8_t frequency:6; + uint16_t bitrate; +} __attribute__ ((packed)) a2dp_mpeg_t; + +#else +#error "Unknown byte order" +#endif + +typedef struct { + uint8_t vendor_id[4]; + uint8_t codec_id[2]; +} __attribute__ ((packed)) a2dp_vendor_codec_t; diff --git a/audio/a2dp.c b/audio/a2dp.c new file mode 100644 index 0000000..a8b3006 --- /dev/null +++ b/audio/a2dp.c @@ -0,0 +1,2489 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * Copyright (C) 2011 BMW Car IT GmbH. All rights reserved. + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include +#include + +#include +#include +#include + +#include "log.h" +#include "device.h" +#include "manager.h" +#include "avdtp.h" +#include "sink.h" +#include "source.h" +#include "unix.h" +#include "a2dp.h" +#include "a2dp-codecs.h" +#include "sdpd.h" + +/* The duration that streams without users are allowed to stay in + * STREAMING state. */ +#define SUSPEND_TIMEOUT 5 +#define RECONFIGURE_TIMEOUT 500 + +#ifndef MIN +# define MIN(x, y) ((x) < (y) ? (x) : (y)) +#endif + +#ifndef MAX +# define MAX(x, y) ((x) > (y) ? (x) : (y)) +#endif + +struct a2dp_sep { + struct a2dp_server *server; + struct a2dp_endpoint *endpoint; + uint8_t type; + uint8_t codec; + struct avdtp_local_sep *lsep; + struct avdtp *session; + struct avdtp_stream *stream; + guint suspend_timer; + gboolean delay_reporting; + gboolean locked; + gboolean suspending; + gboolean starting; + void *user_data; + GDestroyNotify destroy; +}; + +struct a2dp_setup_cb { + struct a2dp_setup *setup; + a2dp_select_cb_t select_cb; + a2dp_config_cb_t config_cb; + a2dp_stream_cb_t resume_cb; + a2dp_stream_cb_t suspend_cb; + guint source_id; + void *user_data; + unsigned int id; +}; + +struct a2dp_setup { + struct audio_device *dev; + struct avdtp *session; + struct a2dp_sep *sep; + struct avdtp_remote_sep *rsep; + struct avdtp_stream *stream; + struct avdtp_error *err; + avdtp_set_configuration_cb setconf_cb; + GSList *caps; + gboolean reconfigure; + gboolean start; + GSList *cb; + int ref; +}; + +static DBusConnection *connection = NULL; + +struct a2dp_server { + bdaddr_t src; + GSList *sinks; + GSList *sources; + uint32_t source_record_id; + uint32_t sink_record_id; + uint16_t version; + gboolean sink_enabled; + gboolean source_enabled; +}; + +static GSList *servers = NULL; +static GSList *setups = NULL; +static unsigned int cb_id = 0; + +static struct a2dp_setup *setup_ref(struct a2dp_setup *setup) +{ + setup->ref++; + + DBG("%p: ref=%d", setup, setup->ref); + + return setup; +} + +static struct audio_device *a2dp_get_dev(struct avdtp *session) +{ + bdaddr_t src, dst; + + avdtp_get_peers(session, &src, &dst); + + return manager_find_device(NULL, &src, &dst, NULL, FALSE); +} + +static struct a2dp_setup *setup_new(struct avdtp *session) +{ + struct audio_device *dev; + struct a2dp_setup *setup; + + dev = a2dp_get_dev(session); + if (!dev) { + error("Unable to create setup"); + return NULL; + } + + setup = g_new0(struct a2dp_setup, 1); + setup->session = avdtp_ref(session); + setup->dev = a2dp_get_dev(session); + setups = g_slist_append(setups, setup); + + return setup; +} + +static void setup_free(struct a2dp_setup *s) +{ + DBG("%p", s); + + setups = g_slist_remove(setups, s); + if (s->session) + avdtp_unref(s->session); + g_slist_free_full(s->cb, g_free); + g_slist_free_full(s->caps, g_free); + g_free(s); +} + +static void setup_unref(struct a2dp_setup *setup) +{ + setup->ref--; + + DBG("%p: ref=%d", setup, setup->ref); + + if (setup->ref > 0) + return; + + setup_free(setup); +} + +static struct a2dp_setup_cb *setup_cb_new(struct a2dp_setup *setup) +{ + struct a2dp_setup_cb *cb; + + cb = g_new0(struct a2dp_setup_cb, 1); + cb->setup = setup; + cb->id = ++cb_id; + + setup->cb = g_slist_append(setup->cb, cb); + return cb; +} + +static void setup_cb_free(struct a2dp_setup_cb *cb) +{ + struct a2dp_setup *setup = cb->setup; + + if (cb->source_id) + g_source_remove(cb->source_id); + + setup->cb = g_slist_remove(setup->cb, cb); + setup_unref(cb->setup); + g_free(cb); +} + +static void finalize_setup_errno(struct a2dp_setup *s, int err, + GSourceFunc cb1, ...) +{ + GSourceFunc finalize; + va_list args; + struct avdtp_error avdtp_err; + + if (err < 0) { + avdtp_error_init(&avdtp_err, AVDTP_ERRNO, -err); + s->err = &avdtp_err; + } + + va_start(args, cb1); + finalize = cb1; + setup_ref(s); + while (finalize != NULL) { + finalize(s); + finalize = va_arg(args, GSourceFunc); + } + setup_unref(s); + va_end(args); +} + +static gboolean finalize_config(gpointer data) +{ + struct a2dp_setup *s = data; + GSList *l; + struct avdtp_stream *stream = s->err ? NULL : s->stream; + + for (l = s->cb; l != NULL; ) { + struct a2dp_setup_cb *cb = l->data; + + l = l->next; + + if (!cb->config_cb) + continue; + + cb->config_cb(s->session, s->sep, stream, s->err, + cb->user_data); + setup_cb_free(cb); + } + + return FALSE; +} + +static gboolean finalize_resume(gpointer data) +{ + struct a2dp_setup *s = data; + GSList *l; + + for (l = s->cb; l != NULL; ) { + struct a2dp_setup_cb *cb = l->data; + + l = l->next; + + if (!cb->resume_cb) + continue; + + cb->resume_cb(s->session, s->err, cb->user_data); + setup_cb_free(cb); + } + + return FALSE; +} + +static gboolean finalize_suspend(gpointer data) +{ + struct a2dp_setup *s = data; + GSList *l; + + for (l = s->cb; l != NULL; ) { + struct a2dp_setup_cb *cb = l->data; + + l = l->next; + + if (!cb->suspend_cb) + continue; + + cb->suspend_cb(s->session, s->err, cb->user_data); + setup_cb_free(cb); + } + + return FALSE; +} + +static void finalize_select(struct a2dp_setup *s) +{ + GSList *l; + + for (l = s->cb; l != NULL; ) { + struct a2dp_setup_cb *cb = l->data; + + l = l->next; + + if (!cb->select_cb) + continue; + + cb->select_cb(s->session, s->sep, s->caps, cb->user_data); + setup_cb_free(cb); + } +} + +static struct a2dp_setup *find_setup_by_session(struct avdtp *session) +{ + GSList *l; + + for (l = setups; l != NULL; l = l->next) { + struct a2dp_setup *setup = l->data; + + if (setup->session == session) + return setup; + } + + return NULL; +} + +static struct a2dp_setup *a2dp_setup_get(struct avdtp *session) +{ + struct a2dp_setup *setup; + + setup = find_setup_by_session(session); + if (!setup) { + setup = setup_new(session); + if (!setup) + return NULL; + } + + return setup_ref(setup); +} + +static struct a2dp_setup *find_setup_by_dev(struct audio_device *dev) +{ + GSList *l; + + for (l = setups; l != NULL; l = l->next) { + struct a2dp_setup *setup = l->data; + + if (setup->dev == dev) + return setup; + } + + return NULL; +} + +static void stream_state_changed(struct avdtp_stream *stream, + avdtp_state_t old_state, + avdtp_state_t new_state, + struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *sep = user_data; + + if (new_state != AVDTP_STATE_IDLE) + return; + + if (sep->suspend_timer) { + g_source_remove(sep->suspend_timer); + sep->suspend_timer = 0; + } + + if (sep->session) { + avdtp_unref(sep->session); + sep->session = NULL; + } + + sep->stream = NULL; + + if (sep->endpoint && sep->endpoint->clear_configuration) + sep->endpoint->clear_configuration(sep, sep->user_data); +} + +static gboolean auto_config(gpointer data) +{ + struct a2dp_setup *setup = data; + struct avdtp_error *err = NULL; + + /* Check if configuration was aborted */ + if (setup->sep->stream == NULL) + return FALSE; + + if (setup->err != NULL) { + err = setup->err; + goto done; + } + + avdtp_stream_add_cb(setup->session, setup->stream, + stream_state_changed, setup->sep); + + if (setup->sep->type == AVDTP_SEP_TYPE_SOURCE) + sink_new_stream(setup->dev, setup->session, setup->stream); + else + source_new_stream(setup->dev, setup->session, setup->stream); + +done: + if (setup->setconf_cb) + setup->setconf_cb(setup->session, setup->stream, setup->err); + + finalize_config(setup); + + if (err) + g_free(err); + + setup_unref(setup); + + return FALSE; +} + +static gboolean sbc_setconf_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + struct avdtp_stream *stream, + GSList *caps, + avdtp_set_configuration_cb cb, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Set_Configuration_Ind", sep); + else + DBG("Source %p: Set_Configuration_Ind", sep); + + setup = a2dp_setup_get(session); + if (!setup) + return FALSE; + + a2dp_sep->stream = stream; + setup->sep = a2dp_sep; + setup->stream = stream; + setup->setconf_cb = cb; + + /* Check valid settings */ + for (; caps != NULL; caps = g_slist_next(caps)) { + struct avdtp_service_capability *cap = caps->data; + struct avdtp_media_codec_capability *codec_cap; + struct sbc_codec_cap *sbc_cap; + + if (cap->category == AVDTP_DELAY_REPORTING && + !a2dp_sep->delay_reporting) { + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_DELAY_REPORTING, + AVDTP_UNSUPPORTED_CONFIGURATION); + goto done; + } + + if (cap->category != AVDTP_MEDIA_CODEC) + continue; + + if (cap->length < sizeof(struct sbc_codec_cap)) + continue; + + codec_cap = (void *) cap->data; + + if (codec_cap->media_codec_type != A2DP_CODEC_SBC) + continue; + + sbc_cap = (void *) codec_cap; + + if (sbc_cap->min_bitpool < MIN_BITPOOL || + sbc_cap->max_bitpool > MAX_BITPOOL) { + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_MEDIA_CODEC, + AVDTP_UNSUPPORTED_CONFIGURATION); + goto done; + } + } + +done: + g_idle_add(auto_config, setup); + return TRUE; +} + +static gboolean sbc_getcap_ind(struct avdtp *session, struct avdtp_local_sep *sep, + gboolean get_all, GSList **caps, uint8_t *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct avdtp_service_capability *media_transport, *media_codec; + struct sbc_codec_cap sbc_cap; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Get_Capability_Ind", sep); + else + DBG("Source %p: Get_Capability_Ind", sep); + + *caps = NULL; + + media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, + NULL, 0); + + *caps = g_slist_append(*caps, media_transport); + + memset(&sbc_cap, 0, sizeof(struct sbc_codec_cap)); + + sbc_cap.cap.media_type = AVDTP_MEDIA_TYPE_AUDIO; + sbc_cap.cap.media_codec_type = A2DP_CODEC_SBC; + + sbc_cap.frequency = ( SBC_SAMPLING_FREQ_48000 | + SBC_SAMPLING_FREQ_44100 | + SBC_SAMPLING_FREQ_32000 | + SBC_SAMPLING_FREQ_16000 ); + + sbc_cap.channel_mode = ( SBC_CHANNEL_MODE_JOINT_STEREO | + SBC_CHANNEL_MODE_STEREO | + SBC_CHANNEL_MODE_DUAL_CHANNEL | + SBC_CHANNEL_MODE_MONO ); + + sbc_cap.block_length = ( SBC_BLOCK_LENGTH_16 | + SBC_BLOCK_LENGTH_12 | + SBC_BLOCK_LENGTH_8 | + SBC_BLOCK_LENGTH_4 ); + + sbc_cap.subbands = ( SBC_SUBBANDS_8 | SBC_SUBBANDS_4 ); + + sbc_cap.allocation_method = ( SBC_ALLOCATION_LOUDNESS | + SBC_ALLOCATION_SNR ); + + sbc_cap.min_bitpool = MIN_BITPOOL; + sbc_cap.max_bitpool = MAX_BITPOOL; + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, &sbc_cap, + sizeof(sbc_cap)); + + *caps = g_slist_append(*caps, media_codec); + + if (get_all) { + struct avdtp_service_capability *delay_reporting; + delay_reporting = avdtp_service_cap_new(AVDTP_DELAY_REPORTING, + NULL, 0); + *caps = g_slist_append(*caps, delay_reporting); + } + + return TRUE; +} + +static gboolean mpeg_setconf_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + struct avdtp_stream *stream, + GSList *caps, + avdtp_set_configuration_cb cb, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Set_Configuration_Ind", sep); + else + DBG("Source %p: Set_Configuration_Ind", sep); + + setup = a2dp_setup_get(session); + if (!setup) + return FALSE; + + a2dp_sep->stream = stream; + setup->sep = a2dp_sep; + setup->stream = stream; + setup->setconf_cb = cb; + + for (; caps != NULL; caps = g_slist_next(caps)) { + struct avdtp_service_capability *cap = caps->data; + + if (cap->category == AVDTP_DELAY_REPORTING && + !a2dp_sep->delay_reporting) { + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_DELAY_REPORTING, + AVDTP_UNSUPPORTED_CONFIGURATION); + goto done; + } + } + +done: + g_idle_add(auto_config, setup); + return TRUE; +} + +static gboolean mpeg_getcap_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + gboolean get_all, + GSList **caps, uint8_t *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct avdtp_service_capability *media_transport, *media_codec; + struct mpeg_codec_cap mpeg_cap; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Get_Capability_Ind", sep); + else + DBG("Source %p: Get_Capability_Ind", sep); + + *caps = NULL; + + media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, + NULL, 0); + + *caps = g_slist_append(*caps, media_transport); + + memset(&mpeg_cap, 0, sizeof(struct mpeg_codec_cap)); + + mpeg_cap.cap.media_type = AVDTP_MEDIA_TYPE_AUDIO; + mpeg_cap.cap.media_codec_type = A2DP_CODEC_MPEG12; + + mpeg_cap.frequency = ( MPEG_SAMPLING_FREQ_48000 | + MPEG_SAMPLING_FREQ_44100 | + MPEG_SAMPLING_FREQ_32000 | + MPEG_SAMPLING_FREQ_24000 | + MPEG_SAMPLING_FREQ_22050 | + MPEG_SAMPLING_FREQ_16000 ); + + mpeg_cap.channel_mode = ( MPEG_CHANNEL_MODE_JOINT_STEREO | + MPEG_CHANNEL_MODE_STEREO | + MPEG_CHANNEL_MODE_DUAL_CHANNEL | + MPEG_CHANNEL_MODE_MONO ); + + mpeg_cap.layer = ( MPEG_LAYER_MP3 | MPEG_LAYER_MP2 | MPEG_LAYER_MP1 ); + + mpeg_cap.bitrate = 0xFFFF; + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, &mpeg_cap, + sizeof(mpeg_cap)); + + *caps = g_slist_append(*caps, media_codec); + + if (get_all) { + struct avdtp_service_capability *delay_reporting; + delay_reporting = avdtp_service_cap_new(AVDTP_DELAY_REPORTING, + NULL, 0); + *caps = g_slist_append(*caps, delay_reporting); + } + + return TRUE; +} + + +static void endpoint_setconf_cb(struct a2dp_setup *setup, gboolean ret) +{ + if (ret == FALSE) { + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_MEDIA_CODEC, + AVDTP_UNSUPPORTED_CONFIGURATION); + } + + auto_config(setup); +} + +static gboolean endpoint_setconf_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + struct avdtp_stream *stream, + GSList *caps, + avdtp_set_configuration_cb cb, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Set_Configuration_Ind", sep); + else + DBG("Source %p: Set_Configuration_Ind", sep); + + setup = a2dp_setup_get(session); + if (!session) + return FALSE; + + a2dp_sep->stream = stream; + setup->sep = a2dp_sep; + setup->stream = stream; + setup->setconf_cb = cb; + + for (; caps != NULL; caps = g_slist_next(caps)) { + struct avdtp_service_capability *cap = caps->data; + struct avdtp_media_codec_capability *codec; + gboolean ret; + + if (cap->category == AVDTP_DELAY_REPORTING && + !a2dp_sep->delay_reporting) { + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_DELAY_REPORTING, + AVDTP_UNSUPPORTED_CONFIGURATION); + goto done; + } + + if (cap->category != AVDTP_MEDIA_CODEC) + continue; + + codec = (struct avdtp_media_codec_capability *) cap->data; + + if (codec->media_codec_type != a2dp_sep->codec) { + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_MEDIA_CODEC, + AVDTP_UNSUPPORTED_CONFIGURATION); + goto done; + } + + ret = a2dp_sep->endpoint->set_configuration(a2dp_sep, + setup->dev, codec->data, + cap->length - sizeof(*codec), + setup, + endpoint_setconf_cb, + a2dp_sep->user_data); + if (ret == 0) + return TRUE; + + setup->err = g_new(struct avdtp_error, 1); + avdtp_error_init(setup->err, AVDTP_MEDIA_CODEC, + AVDTP_UNSUPPORTED_CONFIGURATION); + break; + } + +done: + g_idle_add(auto_config, setup); + return TRUE; +} + +static gboolean endpoint_getcap_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + gboolean get_all, GSList **caps, + uint8_t *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct avdtp_service_capability *media_transport, *media_codec; + struct avdtp_media_codec_capability *codec_caps; + uint8_t *capabilities; + size_t length; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Get_Capability_Ind", sep); + else + DBG("Source %p: Get_Capability_Ind", sep); + + *caps = NULL; + + media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, + NULL, 0); + + *caps = g_slist_append(*caps, media_transport); + + length = a2dp_sep->endpoint->get_capabilities(a2dp_sep, &capabilities, + a2dp_sep->user_data); + + codec_caps = g_malloc0(sizeof(*codec_caps) + length); + codec_caps->media_type = AVDTP_MEDIA_TYPE_AUDIO; + codec_caps->media_codec_type = a2dp_sep->codec; + memcpy(codec_caps->data, capabilities, length); + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, codec_caps, + sizeof(*codec_caps) + length); + + *caps = g_slist_append(*caps, media_codec); + g_free(codec_caps); + + if (get_all) { + struct avdtp_service_capability *delay_reporting; + delay_reporting = avdtp_service_cap_new(AVDTP_DELAY_REPORTING, + NULL, 0); + *caps = g_slist_append(*caps, delay_reporting); + } + + return TRUE; +} + +static void endpoint_open_cb(struct a2dp_setup *setup, gboolean ret) +{ + int err; + + if (ret == FALSE) { + setup->stream = NULL; + finalize_setup_errno(setup, -EPERM, finalize_config, NULL); + return; + } + + err = avdtp_open(setup->session, setup->stream); + if (err == 0) + return; + + error("Error on avdtp_open %s (%d)", strerror(-err), -err); + setup->stream = NULL; + finalize_setup_errno(setup, err, finalize_config, NULL); +} + +static void setconf_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + struct audio_device *dev; + int ret; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Set_Configuration_Cfm", sep); + else + DBG("Source %p: Set_Configuration_Cfm", sep); + + setup = find_setup_by_session(session); + + if (err) { + if (setup) { + setup->err = err; + finalize_config(setup); + } + return; + } + + avdtp_stream_add_cb(session, stream, stream_state_changed, a2dp_sep); + a2dp_sep->stream = stream; + + if (!setup) + return; + + dev = a2dp_get_dev(session); + + /* Notify D-Bus interface of the new stream */ + if (a2dp_sep->type == AVDTP_SEP_TYPE_SOURCE) + sink_new_stream(dev, session, setup->stream); + else + source_new_stream(dev, session, setup->stream); + + /* Notify Endpoint */ + if (a2dp_sep->endpoint) { + struct avdtp_service_capability *service; + struct avdtp_media_codec_capability *codec; + int err; + + service = avdtp_stream_get_codec(stream); + codec = (struct avdtp_media_codec_capability *) service->data; + + err = a2dp_sep->endpoint->set_configuration(a2dp_sep, dev, + codec->data, service->length - + sizeof(*codec), + setup, + endpoint_open_cb, + a2dp_sep->user_data); + if (err == 0) + return; + + setup->stream = NULL; + finalize_setup_errno(setup, -EPERM, finalize_config, NULL); + return; + } + + ret = avdtp_open(session, stream); + if (ret < 0) { + error("Error on avdtp_open %s (%d)", strerror(-ret), -ret); + setup->stream = NULL; + finalize_setup_errno(setup, ret, finalize_config, NULL); + } +} + +static gboolean getconf_ind(struct avdtp *session, struct avdtp_local_sep *sep, + uint8_t *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Get_Configuration_Ind", sep); + else + DBG("Source %p: Get_Configuration_Ind", sep); + return TRUE; +} + +static void getconf_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Set_Configuration_Cfm", sep); + else + DBG("Source %p: Set_Configuration_Cfm", sep); +} + +static gboolean open_ind(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Open_Ind", sep); + else + DBG("Source %p: Open_Ind", sep); + + setup = find_setup_by_session(session); + if (!setup) + return TRUE; + + if (setup->reconfigure) + setup->reconfigure = FALSE; + + finalize_config(setup); + + return TRUE; +} + +static void open_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Open_Cfm", sep); + else + DBG("Source %p: Open_Cfm", sep); + + setup = find_setup_by_session(session); + if (!setup) + return; + + if (setup->reconfigure) + setup->reconfigure = FALSE; + + if (err) { + setup->stream = NULL; + setup->err = err; + } + + finalize_config(setup); +} + +static gboolean suspend_timeout(struct a2dp_sep *sep) +{ + if (avdtp_suspend(sep->session, sep->stream) == 0) + sep->suspending = TRUE; + + sep->suspend_timer = 0; + + avdtp_unref(sep->session); + sep->session = NULL; + + return FALSE; +} + +static gboolean start_ind(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Start_Ind", sep); + else + DBG("Source %p: Start_Ind", sep); + + if (!a2dp_sep->locked) { + a2dp_sep->session = avdtp_ref(session); + a2dp_sep->suspend_timer = g_timeout_add_seconds(SUSPEND_TIMEOUT, + (GSourceFunc) suspend_timeout, + a2dp_sep); + } + + if (!a2dp_sep->starting) + return TRUE; + + a2dp_sep->starting = FALSE; + + setup = find_setup_by_session(session); + if (setup) + finalize_resume(setup); + + return TRUE; +} + +static void start_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Start_Cfm", sep); + else + DBG("Source %p: Start_Cfm", sep); + + a2dp_sep->starting = FALSE; + + setup = find_setup_by_session(session); + if (!setup) + return; + + if (err) { + setup->stream = NULL; + setup->err = err; + } + + finalize_resume(setup); +} + +static gboolean suspend_ind(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + gboolean start; + int start_err; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Suspend_Ind", sep); + else + DBG("Source %p: Suspend_Ind", sep); + + if (a2dp_sep->suspend_timer) { + g_source_remove(a2dp_sep->suspend_timer); + a2dp_sep->suspend_timer = 0; + avdtp_unref(a2dp_sep->session); + a2dp_sep->session = NULL; + } + + if (!a2dp_sep->suspending) + return TRUE; + + a2dp_sep->suspending = FALSE; + + setup = find_setup_by_session(session); + if (!setup) + return TRUE; + + start = setup->start; + setup->start = FALSE; + + finalize_suspend(setup); + + if (!start) + return TRUE; + + start_err = avdtp_start(session, a2dp_sep->stream); + if (start_err < 0 && start_err != -EINPROGRESS) { + error("avdtp_start: %s (%d)", strerror(-start_err), + -start_err); + finalize_setup_errno(setup, start_err, finalize_resume); + } + + return TRUE; +} + +static void suspend_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + gboolean start; + int start_err; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Suspend_Cfm", sep); + else + DBG("Source %p: Suspend_Cfm", sep); + + a2dp_sep->suspending = FALSE; + + setup = find_setup_by_session(session); + if (!setup) + return; + + start = setup->start; + setup->start = FALSE; + + if (err) { + setup->stream = NULL; + setup->err = err; + } + + finalize_suspend(setup); + + if (!start) + return; + + if (err) { + finalize_resume(setup); + return; + } + + start_err = avdtp_start(session, a2dp_sep->stream); + if (start_err < 0 && start_err != -EINPROGRESS) { + error("avdtp_start: %s (%d)", strerror(-start_err), + -start_err); + finalize_setup_errno(setup, start_err, finalize_suspend, NULL); + } +} + +static gboolean close_ind(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Close_Ind", sep); + else + DBG("Source %p: Close_Ind", sep); + + setup = find_setup_by_session(session); + if (!setup) + return TRUE; + + finalize_setup_errno(setup, -ECONNRESET, finalize_suspend, + finalize_resume, NULL); + + return TRUE; +} + +static gboolean a2dp_reconfigure(gpointer data) +{ + struct a2dp_setup *setup = data; + struct a2dp_sep *sep = setup->sep; + int posix_err; + struct avdtp_media_codec_capability *rsep_codec; + struct avdtp_service_capability *cap; + + if (setup->rsep) { + cap = avdtp_get_codec(setup->rsep); + rsep_codec = (struct avdtp_media_codec_capability *) cap->data; + } + + if (!setup->rsep || sep->codec != rsep_codec->media_codec_type) + setup->rsep = avdtp_find_remote_sep(setup->session, sep->lsep); + + posix_err = avdtp_set_configuration(setup->session, setup->rsep, + sep->lsep, + setup->caps, + &setup->stream); + if (posix_err < 0) { + error("avdtp_set_configuration: %s", strerror(-posix_err)); + goto failed; + } + + return FALSE; + +failed: + finalize_setup_errno(setup, posix_err, finalize_config, NULL); + return FALSE; +} + +static void close_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Close_Cfm", sep); + else + DBG("Source %p: Close_Cfm", sep); + + setup = find_setup_by_session(session); + if (!setup) + return; + + if (err) { + setup->stream = NULL; + setup->err = err; + finalize_config(setup); + return; + } + + if (!setup->rsep) + setup->rsep = avdtp_stream_get_remote_sep(stream); + + if (setup->reconfigure) + g_timeout_add(RECONFIGURE_TIMEOUT, a2dp_reconfigure, setup); +} + +static void abort_ind(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Abort_Ind", sep); + else + DBG("Source %p: Abort_Ind", sep); + + a2dp_sep->stream = NULL; + + setup = find_setup_by_session(session); + if (!setup) + return; + + finalize_setup_errno(setup, -ECONNRESET, finalize_suspend, + finalize_resume, + finalize_config); + + return; +} + +static void abort_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: Abort_Cfm", sep); + else + DBG("Source %p: Abort_Cfm", sep); + + setup = find_setup_by_session(session); + if (!setup) + return; + + setup_unref(setup); +} + +static gboolean reconf_ind(struct avdtp *session, struct avdtp_local_sep *sep, + uint8_t *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: ReConfigure_Ind", sep); + else + DBG("Source %p: ReConfigure_Ind", sep); + + return TRUE; +} + +static gboolean delayreport_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + uint8_t rseid, uint16_t delay, + uint8_t *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct audio_device *dev = a2dp_get_dev(session); + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: DelayReport_Ind", sep); + else + DBG("Source %p: DelayReport_Ind", sep); + + unix_delay_report(dev, rseid, delay); + + return TRUE; +} + +static gboolean endpoint_delayreport_ind(struct avdtp *session, + struct avdtp_local_sep *sep, + uint8_t rseid, uint16_t delay, + uint8_t *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: DelayReport_Ind", sep); + else + DBG("Source %p: DelayReport_Ind", sep); + + if (a2dp_sep->endpoint == NULL || + a2dp_sep->endpoint->set_delay == NULL) + return FALSE; + + a2dp_sep->endpoint->set_delay(a2dp_sep, delay, a2dp_sep->user_data); + + return TRUE; +} + +static void reconf_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + struct a2dp_setup *setup; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: ReConfigure_Cfm", sep); + else + DBG("Source %p: ReConfigure_Cfm", sep); + + setup = find_setup_by_session(session); + if (!setup) + return; + + if (err) { + setup->stream = NULL; + setup->err = err; + } + + finalize_config(setup); +} + +static void delay_report_cfm(struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data) +{ + struct a2dp_sep *a2dp_sep = user_data; + + if (a2dp_sep->type == AVDTP_SEP_TYPE_SINK) + DBG("Sink %p: DelayReport_Cfm", sep); + else + DBG("Source %p: DelayReport_Cfm", sep); +} + +static struct avdtp_sep_cfm cfm = { + .set_configuration = setconf_cfm, + .get_configuration = getconf_cfm, + .open = open_cfm, + .start = start_cfm, + .suspend = suspend_cfm, + .close = close_cfm, + .abort = abort_cfm, + .reconfigure = reconf_cfm, + .delay_report = delay_report_cfm, +}; + +static struct avdtp_sep_ind sbc_ind = { + .get_capability = sbc_getcap_ind, + .set_configuration = sbc_setconf_ind, + .get_configuration = getconf_ind, + .open = open_ind, + .start = start_ind, + .suspend = suspend_ind, + .close = close_ind, + .abort = abort_ind, + .reconfigure = reconf_ind, + .delayreport = delayreport_ind, +}; + +static struct avdtp_sep_ind mpeg_ind = { + .get_capability = mpeg_getcap_ind, + .set_configuration = mpeg_setconf_ind, + .get_configuration = getconf_ind, + .open = open_ind, + .start = start_ind, + .suspend = suspend_ind, + .close = close_ind, + .abort = abort_ind, + .reconfigure = reconf_ind, + .delayreport = delayreport_ind, +}; + +static struct avdtp_sep_ind endpoint_ind = { + .get_capability = endpoint_getcap_ind, + .set_configuration = endpoint_setconf_ind, + .get_configuration = getconf_ind, + .open = open_ind, + .start = start_ind, + .suspend = suspend_ind, + .close = close_ind, + .abort = abort_ind, + .reconfigure = reconf_ind, + .delayreport = endpoint_delayreport_ind, +}; + +static sdp_record_t *a2dp_record(uint8_t type, uint16_t avdtp_ver) +{ + sdp_list_t *svclass_id, *pfseq, *apseq, *root; + uuid_t root_uuid, l2cap_uuid, avdtp_uuid, a2dp_uuid; + sdp_profile_desc_t profile[1]; + sdp_list_t *aproto, *proto[2]; + sdp_record_t *record; + sdp_data_t *psm, *version, *features; + uint16_t lp = AVDTP_UUID; + uint16_t a2dp_ver = 0x0102, feat = 0x000f; + + record = sdp_record_alloc(); + if (!record) + return NULL; + + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups(record, root); + + if (type == AVDTP_SEP_TYPE_SOURCE) + sdp_uuid16_create(&a2dp_uuid, AUDIO_SOURCE_SVCLASS_ID); + else + sdp_uuid16_create(&a2dp_uuid, AUDIO_SINK_SVCLASS_ID); + svclass_id = sdp_list_append(0, &a2dp_uuid); + sdp_set_service_classes(record, svclass_id); + + sdp_uuid16_create(&profile[0].uuid, ADVANCED_AUDIO_PROFILE_ID); + profile[0].version = a2dp_ver; + pfseq = sdp_list_append(0, &profile[0]); + sdp_set_profile_descs(record, pfseq); + + sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID); + proto[0] = sdp_list_append(0, &l2cap_uuid); + psm = sdp_data_alloc(SDP_UINT16, &lp); + proto[0] = sdp_list_append(proto[0], psm); + apseq = sdp_list_append(0, proto[0]); + + sdp_uuid16_create(&avdtp_uuid, AVDTP_UUID); + proto[1] = sdp_list_append(0, &avdtp_uuid); + version = sdp_data_alloc(SDP_UINT16, &avdtp_ver); + proto[1] = sdp_list_append(proto[1], version); + apseq = sdp_list_append(apseq, proto[1]); + + aproto = sdp_list_append(0, apseq); + sdp_set_access_protos(record, aproto); + + features = sdp_data_alloc(SDP_UINT16, &feat); + sdp_attr_add(record, SDP_ATTR_SUPPORTED_FEATURES, features); + + if (type == AVDTP_SEP_TYPE_SOURCE) + sdp_set_info_attr(record, "Audio Source", 0, 0); + else + sdp_set_info_attr(record, "Audio Sink", 0, 0); + + free(psm); + free(version); + sdp_list_free(proto[0], 0); + sdp_list_free(proto[1], 0); + sdp_list_free(apseq, 0); + sdp_list_free(pfseq, 0); + sdp_list_free(aproto, 0); + sdp_list_free(root, 0); + sdp_list_free(svclass_id, 0); + + return record; +} + +static struct a2dp_server *find_server(GSList *list, const bdaddr_t *src) +{ + + for (; list; list = list->next) { + struct a2dp_server *server = list->data; + + if (bacmp(&server->src, src) == 0) + return server; + } + + return NULL; +} + +int a2dp_register(DBusConnection *conn, const bdaddr_t *src, GKeyFile *config) +{ + int sbc_srcs = 0, sbc_sinks = 0; + int mpeg12_srcs = 0, mpeg12_sinks = 0; + gboolean source = TRUE, sink = FALSE, socket = FALSE; + gboolean delay_reporting = FALSE; + char *str; + GError *err = NULL; + int i; + struct a2dp_server *server; + + if (!config) + goto proceed; + + str = g_key_file_get_string(config, "General", "Enable", &err); + + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + if (strstr(str, "Sink")) + source = TRUE; + if (strstr(str, "Source")) + sink = TRUE; + if (strstr(str, "Socket")) + socket = TRUE; + g_free(str); + } + + str = g_key_file_get_string(config, "General", "Disable", &err); + + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + if (strstr(str, "Sink")) + source = FALSE; + if (strstr(str, "Source")) + sink = FALSE; + if (strstr(str, "Socket")) + socket = FALSE; + g_free(str); + } + + /* Don't register any local sep if Socket is disabled */ + if (socket == FALSE) + goto proceed; + + str = g_key_file_get_string(config, "A2DP", "SBCSources", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + sbc_srcs = 1; + } else { + sbc_srcs = atoi(str); + g_free(str); + } + + str = g_key_file_get_string(config, "A2DP", "MPEG12Sources", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + mpeg12_srcs = atoi(str); + g_free(str); + } + + str = g_key_file_get_string(config, "A2DP", "SBCSinks", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + sbc_sinks = 1; + } else { + sbc_sinks = atoi(str); + g_free(str); + } + + str = g_key_file_get_string(config, "A2DP", "MPEG12Sinks", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + mpeg12_sinks = atoi(str); + g_free(str); + } + +proceed: + if (!connection) + connection = dbus_connection_ref(conn); + + server = find_server(servers, src); + if (!server) { + int av_err; + + server = g_new0(struct a2dp_server, 1); + if (!server) + return -ENOMEM; + + av_err = avdtp_init(src, config, &server->version); + if (av_err < 0) { + g_free(server); + return av_err; + } + + bacpy(&server->src, src); + servers = g_slist_append(servers, server); + } + + if (config) + delay_reporting = g_key_file_get_boolean(config, "A2DP", + "DelayReporting", NULL); + + if (delay_reporting) + server->version = 0x0103; + else + server->version = 0x0102; + + server->source_enabled = source; + if (source) { + for (i = 0; i < sbc_srcs; i++) + a2dp_add_sep(src, AVDTP_SEP_TYPE_SOURCE, + A2DP_CODEC_SBC, delay_reporting, + NULL, NULL, NULL, NULL); + + for (i = 0; i < mpeg12_srcs; i++) + a2dp_add_sep(src, AVDTP_SEP_TYPE_SOURCE, + A2DP_CODEC_MPEG12, delay_reporting, + NULL, NULL, NULL, NULL); + } + server->sink_enabled = sink; + if (sink) { + for (i = 0; i < sbc_sinks; i++) + a2dp_add_sep(src, AVDTP_SEP_TYPE_SINK, + A2DP_CODEC_SBC, delay_reporting, + NULL, NULL, NULL, NULL); + + for (i = 0; i < mpeg12_sinks; i++) + a2dp_add_sep(src, AVDTP_SEP_TYPE_SINK, + A2DP_CODEC_MPEG12, delay_reporting, + NULL, NULL, NULL, NULL); + } + + return 0; +} + +static void a2dp_unregister_sep(struct a2dp_sep *sep) +{ + if (sep->destroy) { + sep->destroy(sep->user_data); + sep->endpoint = NULL; + } + + avdtp_unregister_sep(sep->lsep); + g_free(sep); +} + +void a2dp_unregister(const bdaddr_t *src) +{ + struct a2dp_server *server; + + server = find_server(servers, src); + if (!server) + return; + + g_slist_free_full(server->sinks, (GDestroyNotify) a2dp_unregister_sep); + g_slist_free_full(server->sources, + (GDestroyNotify) a2dp_unregister_sep); + + avdtp_exit(src); + + servers = g_slist_remove(servers, server); + + if (server->source_record_id) + remove_record_from_server(server->source_record_id); + + if (server->sink_record_id) + remove_record_from_server(server->sink_record_id); + + g_free(server); + + if (servers) + return; + + dbus_connection_unref(connection); + connection = NULL; +} + +struct a2dp_sep *a2dp_add_sep(const bdaddr_t *src, uint8_t type, + uint8_t codec, gboolean delay_reporting, + struct a2dp_endpoint *endpoint, + void *user_data, GDestroyNotify destroy, + int *err) +{ + struct a2dp_server *server; + struct a2dp_sep *sep; + GSList **l; + uint32_t *record_id; + sdp_record_t *record; + struct avdtp_sep_ind *ind; + + server = find_server(servers, src); + if (server == NULL) { + if (err) + *err = -EPROTONOSUPPORT; + return NULL; + } + + if (type == AVDTP_SEP_TYPE_SINK && !server->sink_enabled) { + if (err) + *err = -EPROTONOSUPPORT; + return NULL; + } + + if (type == AVDTP_SEP_TYPE_SOURCE && !server->source_enabled) { + if (err) + *err = -EPROTONOSUPPORT; + return NULL; + } + + sep = g_new0(struct a2dp_sep, 1); + + if (endpoint) { + ind = &endpoint_ind; + goto proceed; + } + + ind = (codec == A2DP_CODEC_MPEG12) ? &mpeg_ind : &sbc_ind; + +proceed: + sep->lsep = avdtp_register_sep(&server->src, type, + AVDTP_MEDIA_TYPE_AUDIO, codec, + delay_reporting, ind, &cfm, sep); + if (sep->lsep == NULL) { + g_free(sep); + if (err) + *err = -EINVAL; + return NULL; + } + + sep->server = server; + sep->endpoint = endpoint; + sep->codec = codec; + sep->type = type; + sep->delay_reporting = delay_reporting; + sep->user_data = user_data; + sep->destroy = destroy; + + if (type == AVDTP_SEP_TYPE_SOURCE) { + l = &server->sources; + record_id = &server->source_record_id; + } else { + l = &server->sinks; + record_id = &server->sink_record_id; + } + + if (*record_id != 0) + goto add; + + record = a2dp_record(type, server->version); + if (!record) { + error("Unable to allocate new service record"); + avdtp_unregister_sep(sep->lsep); + g_free(sep); + if (err) + *err = -EINVAL; + return NULL; + } + + if (add_record_to_server(&server->src, record) < 0) { + error("Unable to register A2DP service record");\ + sdp_record_free(record); + avdtp_unregister_sep(sep->lsep); + g_free(sep); + if (err) + *err = -EINVAL; + return NULL; + } + *record_id = record->handle; + +add: + *l = g_slist_append(*l, sep); + + if (err) + *err = 0; + return sep; +} + +void a2dp_remove_sep(struct a2dp_sep *sep) +{ + struct a2dp_server *server = sep->server; + + if (sep->type == AVDTP_SEP_TYPE_SOURCE) { + if (g_slist_find(server->sources, sep) == NULL) + return; + server->sources = g_slist_remove(server->sources, sep); + if (server->sources == NULL && server->source_record_id) { + remove_record_from_server(server->source_record_id); + server->source_record_id = 0; + } + } else { + if (g_slist_find(server->sinks, sep) == NULL) + return; + server->sinks = g_slist_remove(server->sinks, sep); + if (server->sinks == NULL && server->sink_record_id) { + remove_record_from_server(server->sink_record_id); + server->sink_record_id = 0; + } + } + + if (sep->locked) + return; + + a2dp_unregister_sep(sep); +} + +struct a2dp_sep *a2dp_get(struct avdtp *session, + struct avdtp_remote_sep *rsep) +{ + GSList *l; + struct a2dp_server *server; + struct avdtp_service_capability *cap; + struct avdtp_media_codec_capability *codec_cap = NULL; + bdaddr_t src; + + avdtp_get_peers(session, &src, NULL); + server = find_server(servers, &src); + if (!server) + return NULL; + + cap = avdtp_get_codec(rsep); + codec_cap = (void *) cap->data; + + if (avdtp_get_type(rsep) == AVDTP_SEP_TYPE_SINK) + l = server->sources; + else + l = server->sinks; + + for (; l != NULL; l = l->next) { + struct a2dp_sep *sep = l->data; + + if (sep->locked) + continue; + + if (sep->codec != codec_cap->media_codec_type) + continue; + + if (!sep->stream || avdtp_has_stream(session, sep->stream)) + return sep; + } + + return NULL; +} + +static uint8_t default_bitpool(uint8_t freq, uint8_t mode) +{ + switch (freq) { + case SBC_SAMPLING_FREQ_16000: + case SBC_SAMPLING_FREQ_32000: + return 53; + case SBC_SAMPLING_FREQ_44100: + switch (mode) { + case SBC_CHANNEL_MODE_MONO: + case SBC_CHANNEL_MODE_DUAL_CHANNEL: + return 31; + case SBC_CHANNEL_MODE_STEREO: + case SBC_CHANNEL_MODE_JOINT_STEREO: + return 53; + default: + error("Invalid channel mode %u", mode); + return 53; + } + case SBC_SAMPLING_FREQ_48000: + switch (mode) { + case SBC_CHANNEL_MODE_MONO: + case SBC_CHANNEL_MODE_DUAL_CHANNEL: + return 29; + case SBC_CHANNEL_MODE_STEREO: + case SBC_CHANNEL_MODE_JOINT_STEREO: + return 51; + default: + error("Invalid channel mode %u", mode); + return 51; + } + default: + error("Invalid sampling freq %u", freq); + return 53; + } +} + +static gboolean select_sbc_params(struct sbc_codec_cap *cap, + struct sbc_codec_cap *supported) +{ + unsigned int max_bitpool, min_bitpool; + + memset(cap, 0, sizeof(struct sbc_codec_cap)); + + cap->cap.media_type = AVDTP_MEDIA_TYPE_AUDIO; + cap->cap.media_codec_type = A2DP_CODEC_SBC; + + if (supported->frequency & SBC_SAMPLING_FREQ_44100) + cap->frequency = SBC_SAMPLING_FREQ_44100; + else if (supported->frequency & SBC_SAMPLING_FREQ_48000) + cap->frequency = SBC_SAMPLING_FREQ_48000; + else if (supported->frequency & SBC_SAMPLING_FREQ_32000) + cap->frequency = SBC_SAMPLING_FREQ_32000; + else if (supported->frequency & SBC_SAMPLING_FREQ_16000) + cap->frequency = SBC_SAMPLING_FREQ_16000; + else { + error("No supported frequencies"); + return FALSE; + } + + if (supported->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO) + cap->channel_mode = SBC_CHANNEL_MODE_JOINT_STEREO; + else if (supported->channel_mode & SBC_CHANNEL_MODE_STEREO) + cap->channel_mode = SBC_CHANNEL_MODE_STEREO; + else if (supported->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL) + cap->channel_mode = SBC_CHANNEL_MODE_DUAL_CHANNEL; + else if (supported->channel_mode & SBC_CHANNEL_MODE_MONO) + cap->channel_mode = SBC_CHANNEL_MODE_MONO; + else { + error("No supported channel modes"); + return FALSE; + } + + if (supported->block_length & SBC_BLOCK_LENGTH_16) + cap->block_length = SBC_BLOCK_LENGTH_16; + else if (supported->block_length & SBC_BLOCK_LENGTH_12) + cap->block_length = SBC_BLOCK_LENGTH_12; + else if (supported->block_length & SBC_BLOCK_LENGTH_8) + cap->block_length = SBC_BLOCK_LENGTH_8; + else if (supported->block_length & SBC_BLOCK_LENGTH_4) + cap->block_length = SBC_BLOCK_LENGTH_4; + else { + error("No supported block lengths"); + return FALSE; + } + + if (supported->subbands & SBC_SUBBANDS_8) + cap->subbands = SBC_SUBBANDS_8; + else if (supported->subbands & SBC_SUBBANDS_4) + cap->subbands = SBC_SUBBANDS_4; + else { + error("No supported subbands"); + return FALSE; + } + + if (supported->allocation_method & SBC_ALLOCATION_LOUDNESS) + cap->allocation_method = SBC_ALLOCATION_LOUDNESS; + else if (supported->allocation_method & SBC_ALLOCATION_SNR) + cap->allocation_method = SBC_ALLOCATION_SNR; + + min_bitpool = MAX(MIN_BITPOOL, supported->min_bitpool); + max_bitpool = MIN(default_bitpool(cap->frequency, cap->channel_mode), + supported->max_bitpool); + + cap->min_bitpool = min_bitpool; + cap->max_bitpool = max_bitpool; + + return TRUE; +} + +static gboolean select_capabilities(struct avdtp *session, + struct avdtp_remote_sep *rsep, + GSList **caps) +{ + struct avdtp_service_capability *media_transport, *media_codec; + struct sbc_codec_cap sbc_cap; + + media_codec = avdtp_get_codec(rsep); + if (!media_codec) + return FALSE; + + select_sbc_params(&sbc_cap, (struct sbc_codec_cap *) media_codec->data); + + media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, + NULL, 0); + + *caps = g_slist_append(*caps, media_transport); + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, &sbc_cap, + sizeof(sbc_cap)); + + *caps = g_slist_append(*caps, media_codec); + + if (avdtp_get_delay_reporting(rsep)) { + struct avdtp_service_capability *delay_reporting; + delay_reporting = avdtp_service_cap_new(AVDTP_DELAY_REPORTING, + NULL, 0); + *caps = g_slist_append(*caps, delay_reporting); + } + + return TRUE; +} + +static void select_cb(struct a2dp_setup *setup, void *ret, int size) +{ + struct avdtp_service_capability *media_transport, *media_codec; + struct avdtp_media_codec_capability *cap; + + if (size < 0) { + DBG("Endpoint replied an invalid configuration"); + goto done; + } + + media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, + NULL, 0); + + setup->caps = g_slist_append(setup->caps, media_transport); + + cap = g_malloc0(sizeof(*cap) + size); + cap->media_type = AVDTP_MEDIA_TYPE_AUDIO; + cap->media_codec_type = setup->sep->codec; + memcpy(cap->data, ret, size); + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, cap, + sizeof(*cap) + size); + + setup->caps = g_slist_append(setup->caps, media_codec); + g_free(cap); + +done: + finalize_select(setup); +} + +static gboolean auto_select(gpointer data) +{ + struct a2dp_setup *setup = data; + + finalize_select(setup); + + return FALSE; +} + +static gboolean check_vendor_codec(struct a2dp_sep *sep, uint8_t *cap, + size_t len) +{ + uint8_t *capabilities; + size_t length; + a2dp_vendor_codec_t *local_codec; + a2dp_vendor_codec_t *remote_codec; + + if (len < sizeof(a2dp_vendor_codec_t)) + return FALSE; + + remote_codec = (a2dp_vendor_codec_t *) cap; + + if (sep->endpoint == NULL) + return FALSE; + + length = sep->endpoint->get_capabilities(sep, + &capabilities, sep->user_data); + + if (length < sizeof(a2dp_vendor_codec_t)) + return FALSE; + + local_codec = (a2dp_vendor_codec_t *) capabilities; + + if (memcmp(remote_codec->vendor_id, local_codec->vendor_id, + sizeof(local_codec->vendor_id))) + return FALSE; + + if (memcmp(remote_codec->codec_id, local_codec->codec_id, + sizeof(local_codec->codec_id))) + return FALSE; + + DBG("vendor 0x%02x%02x%02x%02x codec 0x%02x%02x", + remote_codec->vendor_id[0], remote_codec->vendor_id[1], + remote_codec->vendor_id[2], remote_codec->vendor_id[3], + remote_codec->codec_id[0], remote_codec->codec_id[1]); + + return TRUE; +} + +static struct a2dp_sep *a2dp_find_sep(struct avdtp *session, GSList *list, + const char *sender) +{ + for (; list; list = list->next) { + struct a2dp_sep *sep = list->data; + struct avdtp_remote_sep *rsep; + struct avdtp_media_codec_capability *cap; + struct avdtp_service_capability *service; + + /* Use sender's endpoint if available */ + if (sender) { + const char *name; + + if (sep->endpoint == NULL) + continue; + + name = sep->endpoint->get_name(sep, sep->user_data); + if (g_strcmp0(sender, name) != 0) + continue; + } + + rsep = avdtp_find_remote_sep(session, sep->lsep); + if (rsep == NULL) + continue; + + service = avdtp_get_codec(rsep); + cap = (struct avdtp_media_codec_capability *) service->data; + + if (cap->media_codec_type != A2DP_CODEC_VENDOR) + return sep; + + if (check_vendor_codec(sep, cap->data, + service->length - sizeof(*cap))) + return sep; + } + + return NULL; +} + +static struct a2dp_sep *a2dp_select_sep(struct avdtp *session, uint8_t type, + const char *sender) +{ + struct a2dp_server *server; + struct a2dp_sep *sep; + GSList *l; + bdaddr_t src; + + avdtp_get_peers(session, &src, NULL); + server = find_server(servers, &src); + if (!server) + return NULL; + + l = type == AVDTP_SEP_TYPE_SINK ? server->sources : server->sinks; + + /* Check sender's seps first */ + sep = a2dp_find_sep(session, l, sender); + if (sep != NULL) + return sep; + + return a2dp_find_sep(session, l, NULL); +} + +unsigned int a2dp_select_capabilities(struct avdtp *session, + uint8_t type, const char *sender, + a2dp_select_cb_t cb, + void *user_data) +{ + struct a2dp_setup *setup; + struct a2dp_setup_cb *cb_data; + struct a2dp_sep *sep; + struct avdtp_service_capability *service; + struct avdtp_media_codec_capability *codec; + int err; + + sep = a2dp_select_sep(session, type, sender); + if (!sep) { + error("Unable to select SEP"); + return 0; + } + + setup = a2dp_setup_get(session); + if (!setup) + return 0; + + cb_data = setup_cb_new(setup); + cb_data->select_cb = cb; + cb_data->user_data = user_data; + + setup->sep = sep; + setup->rsep = avdtp_find_remote_sep(session, sep->lsep); + + if (setup->rsep == NULL) { + error("Could not find remote sep"); + goto fail; + } + + /* FIXME: Remove auto select when it is not longer possible to register + endpoint in the configuration file */ + if (sep->endpoint == NULL) { + if (!select_capabilities(session, setup->rsep, + &setup->caps)) { + error("Unable to auto select remote SEP capabilities"); + goto fail; + } + + g_idle_add(auto_select, setup); + + return cb_data->id; + } + + service = avdtp_get_codec(setup->rsep); + codec = (struct avdtp_media_codec_capability *) service->data; + + err = sep->endpoint->select_configuration(sep, codec->data, + service->length - sizeof(*codec), + setup, + select_cb, sep->user_data); + if (err == 0) + return cb_data->id; + +fail: + setup_cb_free(cb_data); + return 0; + +} + +unsigned int a2dp_config(struct avdtp *session, struct a2dp_sep *sep, + a2dp_config_cb_t cb, GSList *caps, + void *user_data) +{ + struct a2dp_setup_cb *cb_data; + GSList *l; + struct a2dp_server *server; + struct a2dp_setup *setup; + struct a2dp_sep *tmp; + struct avdtp_service_capability *cap; + struct avdtp_media_codec_capability *codec_cap = NULL; + int posix_err; + bdaddr_t src; + + avdtp_get_peers(session, &src, NULL); + server = find_server(servers, &src); + if (!server) + return 0; + + for (l = caps; l != NULL; l = l->next) { + cap = l->data; + + if (cap->category != AVDTP_MEDIA_CODEC) + continue; + + codec_cap = (void *) cap->data; + break; + } + + if (!codec_cap) + return 0; + + if (sep->codec != codec_cap->media_codec_type) + return 0; + + DBG("a2dp_config: selected SEP %p", sep->lsep); + + setup = a2dp_setup_get(session); + if (!setup) + return 0; + + cb_data = setup_cb_new(setup); + cb_data->config_cb = cb; + cb_data->user_data = user_data; + + setup->sep = sep; + setup->stream = sep->stream; + + /* Copy given caps if they are different than current caps */ + if (setup->caps != caps) { + g_slist_free_full(setup->caps, g_free); + setup->caps = g_slist_copy(caps); + } + + switch (avdtp_sep_get_state(sep->lsep)) { + case AVDTP_STATE_IDLE: + if (sep->type == AVDTP_SEP_TYPE_SOURCE) + l = server->sources; + else + l = server->sinks; + + for (; l != NULL; l = l->next) { + tmp = l->data; + if (avdtp_has_stream(session, tmp->stream)) + break; + } + + if (l != NULL) { + if (a2dp_sep_get_lock(tmp)) + goto failed; + setup->reconfigure = TRUE; + if (avdtp_close(session, tmp->stream, FALSE) < 0) { + error("avdtp_close failed"); + goto failed; + } + break; + } + + setup->rsep = avdtp_find_remote_sep(session, sep->lsep); + if (setup->rsep == NULL) { + error("No matching ACP and INT SEPs found"); + goto failed; + } + + posix_err = avdtp_set_configuration(session, setup->rsep, + sep->lsep, caps, + &setup->stream); + if (posix_err < 0) { + error("avdtp_set_configuration: %s", + strerror(-posix_err)); + goto failed; + } + break; + case AVDTP_STATE_OPEN: + case AVDTP_STATE_STREAMING: + if (avdtp_stream_has_capabilities(setup->stream, caps)) { + DBG("Configuration match: resuming"); + cb_data->source_id = g_idle_add(finalize_config, + setup); + } else if (!setup->reconfigure) { + setup->reconfigure = TRUE; + if (avdtp_close(session, sep->stream, FALSE) < 0) { + error("avdtp_close failed"); + goto failed; + } + } + break; + default: + error("SEP in bad state for requesting a new stream"); + goto failed; + } + + return cb_data->id; + +failed: + setup_cb_free(cb_data); + return 0; +} + +unsigned int a2dp_resume(struct avdtp *session, struct a2dp_sep *sep, + a2dp_stream_cb_t cb, void *user_data) +{ + struct a2dp_setup_cb *cb_data; + struct a2dp_setup *setup; + + setup = a2dp_setup_get(session); + if (!setup) + return 0; + + cb_data = setup_cb_new(setup); + cb_data->resume_cb = cb; + cb_data->user_data = user_data; + + setup->sep = sep; + setup->stream = sep->stream; + + switch (avdtp_sep_get_state(sep->lsep)) { + case AVDTP_STATE_IDLE: + goto failed; + break; + case AVDTP_STATE_OPEN: + if (avdtp_start(session, sep->stream) < 0) { + error("avdtp_start failed"); + goto failed; + } + sep->starting = TRUE; + break; + case AVDTP_STATE_STREAMING: + if (!sep->suspending && sep->suspend_timer) { + g_source_remove(sep->suspend_timer); + sep->suspend_timer = 0; + avdtp_unref(sep->session); + sep->session = NULL; + } + if (sep->suspending) + setup->start = TRUE; + else + cb_data->source_id = g_idle_add(finalize_resume, + setup); + break; + default: + error("SEP in bad state for resume"); + goto failed; + } + + return cb_data->id; + +failed: + setup_cb_free(cb_data); + return 0; +} + +unsigned int a2dp_suspend(struct avdtp *session, struct a2dp_sep *sep, + a2dp_stream_cb_t cb, void *user_data) +{ + struct a2dp_setup_cb *cb_data; + struct a2dp_setup *setup; + + setup = a2dp_setup_get(session); + if (!setup) + return 0; + + cb_data = setup_cb_new(setup); + cb_data->suspend_cb = cb; + cb_data->user_data = user_data; + + setup->sep = sep; + setup->stream = sep->stream; + + switch (avdtp_sep_get_state(sep->lsep)) { + case AVDTP_STATE_IDLE: + error("a2dp_suspend: no stream to suspend"); + goto failed; + break; + case AVDTP_STATE_OPEN: + cb_data->source_id = g_idle_add(finalize_suspend, setup); + break; + case AVDTP_STATE_STREAMING: + if (avdtp_suspend(session, sep->stream) < 0) { + error("avdtp_suspend failed"); + goto failed; + } + sep->suspending = TRUE; + break; + default: + error("SEP in bad state for suspend"); + goto failed; + } + + return cb_data->id; + +failed: + setup_cb_free(cb_data); + return 0; +} + +gboolean a2dp_cancel(struct audio_device *dev, unsigned int id) +{ + struct a2dp_setup *setup; + GSList *l; + + setup = find_setup_by_dev(dev); + if (!setup) + return FALSE; + + for (l = setup->cb; l != NULL; l = g_slist_next(l)) { + struct a2dp_setup_cb *cb = l->data; + + if (cb->id != id) + continue; + + setup_ref(setup); + setup_cb_free(cb); + + if (!setup->cb) { + DBG("aborting setup %p", setup); + avdtp_abort(setup->session, setup->stream); + return TRUE; + } + + setup_unref(setup); + return TRUE; + } + + return FALSE; +} + +gboolean a2dp_sep_lock(struct a2dp_sep *sep, struct avdtp *session) +{ + if (sep->locked) + return FALSE; + + DBG("SEP %p locked", sep->lsep); + sep->locked = TRUE; + + return TRUE; +} + +gboolean a2dp_sep_unlock(struct a2dp_sep *sep, struct avdtp *session) +{ + struct a2dp_server *server = sep->server; + avdtp_state_t state; + GSList *l; + + state = avdtp_sep_get_state(sep->lsep); + + sep->locked = FALSE; + + DBG("SEP %p unlocked", sep->lsep); + + if (sep->type == AVDTP_SEP_TYPE_SOURCE) + l = server->sources; + else + l = server->sinks; + + /* Unregister sep if it was removed */ + if (g_slist_find(l, sep) == NULL) { + a2dp_unregister_sep(sep); + return TRUE; + } + + if (!sep->stream || state == AVDTP_STATE_IDLE) + return TRUE; + + switch (state) { + case AVDTP_STATE_OPEN: + /* Set timer here */ + break; + case AVDTP_STATE_STREAMING: + if (avdtp_suspend(session, sep->stream) == 0) + sep->suspending = TRUE; + break; + default: + break; + } + + return TRUE; +} + +gboolean a2dp_sep_get_lock(struct a2dp_sep *sep) +{ + return sep->locked; +} + +static int stream_cmp(gconstpointer data, gconstpointer user_data) +{ + const struct a2dp_sep *sep = data; + const struct avdtp_stream *stream = user_data; + + return (sep->stream != stream); +} + +struct a2dp_sep *a2dp_get_sep(struct avdtp *session, + struct avdtp_stream *stream) +{ + struct a2dp_server *server; + bdaddr_t src, dst; + GSList *l; + + avdtp_get_peers(session, &src, &dst); + + for (l = servers; l; l = l->next) { + server = l->data; + + if (bacmp(&src, &server->src) == 0) + break; + } + + if (!l) + return NULL; + + l = g_slist_find_custom(server->sources, stream, stream_cmp); + if (l) + return l->data; + + l = g_slist_find_custom(server->sinks, stream, stream_cmp); + if (l) + return l->data; + + return NULL; +} + +struct avdtp_stream *a2dp_sep_get_stream(struct a2dp_sep *sep) +{ + return sep->stream; +} diff --git a/audio/a2dp.h b/audio/a2dp.h new file mode 100644 index 0000000..887c5ac --- /dev/null +++ b/audio/a2dp.h @@ -0,0 +1,194 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * Copyright (C) 2011 BMW Car IT GmbH. All rights reserved. + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define A2DP_CODEC_SBC 0x00 +#define A2DP_CODEC_MPEG12 0x01 +#define A2DP_CODEC_MPEG24 0x02 +#define A2DP_CODEC_ATRAC 0x03 + +#define SBC_SAMPLING_FREQ_16000 (1 << 3) +#define SBC_SAMPLING_FREQ_32000 (1 << 2) +#define SBC_SAMPLING_FREQ_44100 (1 << 1) +#define SBC_SAMPLING_FREQ_48000 1 + +#define SBC_CHANNEL_MODE_MONO (1 << 3) +#define SBC_CHANNEL_MODE_DUAL_CHANNEL (1 << 2) +#define SBC_CHANNEL_MODE_STEREO (1 << 1) +#define SBC_CHANNEL_MODE_JOINT_STEREO 1 + +#define SBC_BLOCK_LENGTH_4 (1 << 3) +#define SBC_BLOCK_LENGTH_8 (1 << 2) +#define SBC_BLOCK_LENGTH_12 (1 << 1) +#define SBC_BLOCK_LENGTH_16 1 + +#define SBC_SUBBANDS_4 (1 << 1) +#define SBC_SUBBANDS_8 1 + +#define SBC_ALLOCATION_SNR (1 << 1) +#define SBC_ALLOCATION_LOUDNESS 1 + +#define MPEG_CHANNEL_MODE_MONO (1 << 3) +#define MPEG_CHANNEL_MODE_DUAL_CHANNEL (1 << 2) +#define MPEG_CHANNEL_MODE_STEREO (1 << 1) +#define MPEG_CHANNEL_MODE_JOINT_STEREO 1 + +#define MPEG_LAYER_MP1 (1 << 2) +#define MPEG_LAYER_MP2 (1 << 1) +#define MPEG_LAYER_MP3 1 + +#define MPEG_SAMPLING_FREQ_16000 (1 << 5) +#define MPEG_SAMPLING_FREQ_22050 (1 << 4) +#define MPEG_SAMPLING_FREQ_24000 (1 << 3) +#define MPEG_SAMPLING_FREQ_32000 (1 << 2) +#define MPEG_SAMPLING_FREQ_44100 (1 << 1) +#define MPEG_SAMPLING_FREQ_48000 1 + +#define MAX_BITPOOL 64 +#define MIN_BITPOOL 2 + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct sbc_codec_cap { + struct avdtp_media_codec_capability cap; + uint8_t channel_mode:4; + uint8_t frequency:4; + uint8_t allocation_method:2; + uint8_t subbands:2; + uint8_t block_length:4; + uint8_t min_bitpool; + uint8_t max_bitpool; +} __attribute__ ((packed)); + +struct mpeg_codec_cap { + struct avdtp_media_codec_capability cap; + uint8_t channel_mode:4; + uint8_t crc:1; + uint8_t layer:3; + uint8_t frequency:6; + uint8_t mpf:1; + uint8_t rfa:1; + uint16_t bitrate; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct sbc_codec_cap { + struct avdtp_media_codec_capability cap; + uint8_t frequency:4; + uint8_t channel_mode:4; + uint8_t block_length:4; + uint8_t subbands:2; + uint8_t allocation_method:2; + uint8_t min_bitpool; + uint8_t max_bitpool; +} __attribute__ ((packed)); + +struct mpeg_codec_cap { + struct avdtp_media_codec_capability cap; + uint8_t layer:3; + uint8_t crc:1; + uint8_t channel_mode:4; + uint8_t rfa:1; + uint8_t mpf:1; + uint8_t frequency:6; + uint16_t bitrate; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif + +struct a2dp_sep; +struct a2dp_setup; + +typedef void (*a2dp_endpoint_select_t) (struct a2dp_setup *setup, void *ret, + int size); +typedef void (*a2dp_endpoint_config_t) (struct a2dp_setup *setup, gboolean ret); + +struct a2dp_endpoint { + const char *(*get_name) (struct a2dp_sep *sep, void *user_data); + size_t (*get_capabilities) (struct a2dp_sep *sep, + uint8_t **capabilities, + void *user_data); + int (*select_configuration) (struct a2dp_sep *sep, + uint8_t *capabilities, + size_t length, + struct a2dp_setup *setup, + a2dp_endpoint_select_t cb, + void *user_data); + int (*set_configuration) (struct a2dp_sep *sep, + struct audio_device *dev, + uint8_t *configuration, + size_t length, + struct a2dp_setup *setup, + a2dp_endpoint_config_t cb, + void *user_data); + void (*clear_configuration) (struct a2dp_sep *sep, void *user_data); + void (*set_delay) (struct a2dp_sep *sep, uint16_t delay, + void *user_data); +}; + +typedef void (*a2dp_select_cb_t) (struct avdtp *session, + struct a2dp_sep *sep, GSList *caps, + void *user_data); +typedef void (*a2dp_config_cb_t) (struct avdtp *session, struct a2dp_sep *sep, + struct avdtp_stream *stream, + struct avdtp_error *err, + void *user_data); +typedef void (*a2dp_stream_cb_t) (struct avdtp *session, + struct avdtp_error *err, + void *user_data); + +int a2dp_register(DBusConnection *conn, const bdaddr_t *src, GKeyFile *config); +void a2dp_unregister(const bdaddr_t *src); + +struct a2dp_sep *a2dp_add_sep(const bdaddr_t *src, uint8_t type, + uint8_t codec, gboolean delay_reporting, + struct a2dp_endpoint *endpoint, + void *user_data, GDestroyNotify destroy, + int *err); +void a2dp_remove_sep(struct a2dp_sep *sep); + +struct a2dp_sep *a2dp_get(struct avdtp *session, struct avdtp_remote_sep *sep); + +unsigned int a2dp_select_capabilities(struct avdtp *session, + uint8_t type, const char *sender, + a2dp_select_cb_t cb, + void *user_data); +unsigned int a2dp_config(struct avdtp *session, struct a2dp_sep *sep, + a2dp_config_cb_t cb, GSList *caps, + void *user_data); +unsigned int a2dp_resume(struct avdtp *session, struct a2dp_sep *sep, + a2dp_stream_cb_t cb, void *user_data); +unsigned int a2dp_suspend(struct avdtp *session, struct a2dp_sep *sep, + a2dp_stream_cb_t cb, void *user_data); +gboolean a2dp_cancel(struct audio_device *dev, unsigned int id); + +gboolean a2dp_sep_lock(struct a2dp_sep *sep, struct avdtp *session); +gboolean a2dp_sep_unlock(struct a2dp_sep *sep, struct avdtp *session); +gboolean a2dp_sep_get_lock(struct a2dp_sep *sep); +struct avdtp_stream *a2dp_sep_get_stream(struct a2dp_sep *sep); +struct a2dp_sep *a2dp_get_sep(struct avdtp *session, + struct avdtp_stream *stream); diff --git a/audio/audio.conf b/audio/audio.conf new file mode 100644 index 0000000..d955f79 --- /dev/null +++ b/audio/audio.conf @@ -0,0 +1,53 @@ +# Configuration file for the audio service + +# This section contains options which are not specific to any +# particular interface +[General] + +# Switch to master role for incoming connections (defaults to true) +#Master=true + +# If we want to disable support for specific services +# Defaults to supporting all implemented services +#Disable=Gateway,Source,Socket + +#ifdef __TIZEN_PATCH__ +Disable=Socket +#endif + +# SCO routing. Either PCM or HCI (in which case audio is routed to/from ALSA) +#ifdef __TIZEN_PATCH__ +SCORouting=PCM +#else +# Defaults to HCI +#SCORouting=PCM +#endif + +# Automatically connect both A2DP and HFP/HSP profiles for incoming +# connections. Some headsets that support both profiles will only connect the +# other one automatically so the default setting of true is usually a good +# idea. +#AutoConnect=true + +# Headset interface specific options (i.e. options which affect how the audio +# service interacts with remote headset devices) +[Headset] + +# Set to true to support HFP, false means only HSP is supported +# Defaults to true +HFP=true + +# Maximum number of connected HSP/HFP devices per adapter. Defaults to 1 +MaxConnected=1 + +# Set to true to enable use of fast connectable mode (faster page scanning) +# for HFP when incoming call starts. Default settings are restored after +# call is answered or rejected. Page scan interval is much shorter and page +# scan type changed to interlaced. Such allows faster connection initiated +# by a headset. +FastConnectable=false + +# Just an example of potential config options for the other interfaces +#[A2DP] +#SBCSources=1 +#MPEG12Sources=0 diff --git a/audio/avctp.c b/audio/avctp.c new file mode 100644 index 0000000..07c2673 --- /dev/null +++ b/audio/avctp.c @@ -0,0 +1,1115 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * Copyright (C) 2011 Texas Instruments, Inc. + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "adapter.h" +#include "../src/device.h" + +#include "log.h" +#include "error.h" +#include "uinput.h" +#include "btio.h" +#include "manager.h" +#include "device.h" +#include "avctp.h" +#include "avrcp.h" + +#define QUIRK_NO_RELEASE 1 << 0 + +/* Message types */ +#define AVCTP_COMMAND 0 +#define AVCTP_RESPONSE 1 + +/* Packet types */ +#define AVCTP_PACKET_SINGLE 0 +#define AVCTP_PACKET_START 1 +#define AVCTP_PACKET_CONTINUE 2 +#define AVCTP_PACKET_END 3 + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct avctp_header { + uint8_t ipid:1; + uint8_t cr:1; + uint8_t packet_type:2; + uint8_t transaction:4; + uint16_t pid; +} __attribute__ ((packed)); +#define AVCTP_HEADER_LENGTH 3 + +struct avc_header { + uint8_t code:4; + uint8_t _hdr0:4; + uint8_t subunit_id:3; + uint8_t subunit_type:5; + uint8_t opcode; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct avctp_header { + uint8_t transaction:4; + uint8_t packet_type:2; + uint8_t cr:1; + uint8_t ipid:1; + uint16_t pid; +} __attribute__ ((packed)); +#define AVCTP_HEADER_LENGTH 3 + +struct avc_header { + uint8_t _hdr0:4; + uint8_t code:4; + uint8_t subunit_type:5; + uint8_t subunit_id:3; + uint8_t opcode; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif + +struct avctp_state_callback { + avctp_state_cb cb; + void *user_data; + unsigned int id; +}; + +struct avctp_server { + bdaddr_t src; + GIOChannel *io; + GSList *sessions; +}; + +struct avctp_rsp_handler { + uint8_t id; + avctp_rsp_cb func; + void *user_data; +}; + +struct avctp { + struct avctp_server *server; + bdaddr_t dst; + + avctp_state_t state; + + int uinput; + + GIOChannel *io; + guint io_id; + + uint16_t mtu; + + uint8_t key_quirks[256]; + GSList *handlers; +}; + +struct avctp_pdu_handler { + uint8_t opcode; + avctp_pdu_cb cb; + void *user_data; + unsigned int id; +}; + +static struct { + const char *name; + uint8_t avc; + uint16_t uinput; +} key_map[] = { + { "PLAY", PLAY_OP, KEY_PLAYCD }, + { "STOP", STAVC_OP_OP, KEY_STOPCD }, + { "PAUSE", PAUSE_OP, KEY_PAUSECD }, + { "FORWARD", FORWARD_OP, KEY_NEXTSONG }, + { "BACKWARD", BACKWARD_OP, KEY_PREVIOUSSONG }, + { "REWIND", REWIND_OP, KEY_REWIND }, + { "FAST FORWARD", FAST_FORWARD_OP, KEY_FASTFORWARD }, + { NULL } +}; + +static GSList *callbacks = NULL; +static GSList *servers = NULL; +static GSList *handlers = NULL; +static uint8_t id = 0; + +static void auth_cb(DBusError *derr, void *user_data); + +static int send_event(int fd, uint16_t type, uint16_t code, int32_t value) +{ + struct uinput_event event; + + memset(&event, 0, sizeof(event)); + event.type = type; + event.code = code; + event.value = value; + + return write(fd, &event, sizeof(event)); +} + +static void send_key(int fd, uint16_t key, int pressed) +{ + if (fd < 0) + return; + + send_event(fd, EV_KEY, key, pressed); + send_event(fd, EV_SYN, SYN_REPORT, 0); +} + +static size_t handle_panel_passthrough(struct avctp *session, + uint8_t transaction, uint8_t *code, + uint8_t *subunit, uint8_t *operands, + size_t operand_count, void *user_data) +{ + const char *status; + int pressed, i; + + if (*code != AVC_CTYPE_CONTROL || *subunit != AVC_SUBUNIT_PANEL) { + *code = AVC_CTYPE_REJECTED; + return 0; + } + + if (operand_count == 0) + goto done; + + if (operands[0] & 0x80) { + status = "released"; + pressed = 0; + } else { + status = "pressed"; + pressed = 1; + } + + for (i = 0; key_map[i].name != NULL; i++) { + uint8_t key_quirks; + + if ((operands[0] & 0x7F) != key_map[i].avc) + continue; + + DBG("AV/C: %s %s", key_map[i].name, status); + + key_quirks = session->key_quirks[key_map[i].avc]; + + if (key_quirks & QUIRK_NO_RELEASE) { + if (!pressed) { + DBG("AV/C: Ignoring release"); + break; + } + + DBG("AV/C: treating key press as press + release"); + send_key(session->uinput, key_map[i].uinput, 1); + send_key(session->uinput, key_map[i].uinput, 0); + break; + } + + send_key(session->uinput, key_map[i].uinput, pressed); + break; + } + + if (key_map[i].name == NULL) { + DBG("AV/C: unknown button 0x%02X %s", + operands[0] & 0x7F, status); + *code = AVC_CTYPE_NOT_IMPLEMENTED; + return 0; + } + +done: + *code = AVC_CTYPE_ACCEPTED; + return operand_count; +} + +static size_t handle_unit_info(struct avctp *session, + uint8_t transaction, uint8_t *code, + uint8_t *subunit, uint8_t *operands, + size_t operand_count, void *user_data) +{ + if (*code != AVC_CTYPE_STATUS) { + *code = AVC_CTYPE_REJECTED; + return 0; + } + + *code = AVC_CTYPE_STABLE; + + /* The first operand should be 0x07 for the UNITINFO response. + * Neither AVRCP (section 22.1, page 117) nor AVC Digital + * Interface Command Set (section 9.2.1, page 45) specs + * explain this value but both use it */ + if (operand_count >= 1) + operands[0] = 0x07; + if (operand_count >= 2) + operands[1] = AVC_SUBUNIT_PANEL << 3; + + DBG("reply to AVC_OP_UNITINFO"); + + return operand_count; +} + +static size_t handle_subunit_info(struct avctp *session, + uint8_t transaction, uint8_t *code, + uint8_t *subunit, uint8_t *operands, + size_t operand_count, void *user_data) +{ + if (*code != AVC_CTYPE_STATUS) { + *code = AVC_CTYPE_REJECTED; + return 0; + } + + *code = AVC_CTYPE_STABLE; + + /* The first operand should be 0x07 for the UNITINFO response. + * Neither AVRCP (section 22.1, page 117) nor AVC Digital + * Interface Command Set (section 9.2.1, page 45) specs + * explain this value but both use it */ + if (operand_count >= 2) + operands[1] = AVC_SUBUNIT_PANEL << 3; + + DBG("reply to AVC_OP_SUBUNITINFO"); + + return operand_count; +} + +static struct avctp_pdu_handler *find_handler(GSList *list, uint8_t opcode) +{ + for (; list; list = list->next) { + struct avctp_pdu_handler *handler = list->data; + + if (handler->opcode == opcode) + return handler; + } + + return NULL; +} + +static void avctp_disconnected(struct avctp *session) +{ + struct avctp_server *server; + + if (!session) + return; + + if (session->io) { + g_io_channel_shutdown(session->io, TRUE, NULL); + g_io_channel_unref(session->io); + session->io = NULL; + } + + if (session->io_id) { + g_source_remove(session->io_id); + session->io_id = 0; + + if (session->state == AVCTP_STATE_CONNECTING) { + struct audio_device *dev; + + dev = manager_get_device(&session->server->src, + &session->dst, FALSE); + audio_device_cancel_authorization(dev, auth_cb, + session); + } + } + + if (session->uinput >= 0) { + char address[18]; + + ba2str(&session->dst, address); + DBG("AVCTP: closing uinput for %s", address); + + ioctl(session->uinput, UI_DEV_DESTROY); + close(session->uinput); + session->uinput = -1; + } + + server = session->server; + server->sessions = g_slist_remove(server->sessions, session); + g_slist_free_full(session->handlers, g_free); + g_free(session); +} + +static void avctp_set_state(struct avctp *session, avctp_state_t new_state) +{ + GSList *l; + struct audio_device *dev; + avctp_state_t old_state = session->state; + + dev = manager_get_device(&session->server->src, &session->dst, FALSE); + if (dev == NULL) { + error("avdtp_set_state(): no matching audio device"); + return; + } + + session->state = new_state; + + for (l = callbacks; l != NULL; l = l->next) { + struct avctp_state_callback *cb = l->data; + cb->cb(dev, old_state, new_state, cb->user_data); + } + + switch (new_state) { + case AVCTP_STATE_DISCONNECTED: + DBG("AVCTP Disconnected"); + + avctp_disconnected(session); + + if (old_state != AVCTP_STATE_CONNECTED) + break; + + if (!audio_device_is_active(dev, NULL)) + audio_device_set_authorized(dev, FALSE); + + break; + case AVCTP_STATE_CONNECTING: + DBG("AVCTP Connecting"); + break; + case AVCTP_STATE_CONNECTED: + DBG("AVCTP Connected"); + break; + default: + error("Invalid AVCTP state %d", new_state); + return; + } +} + +static void handle_response(struct avctp *session, struct avctp_header *avctp, + struct avc_header *avc, uint8_t *operands, + size_t operand_count) +{ + GSList *l; + + for (l = session->handlers; l; l = l->next) { + struct avctp_rsp_handler *handler = l->data; + + if (handler->id != avctp->transaction) + continue; + + if (handler->func && handler->func(session, avc->code, + avc->subunit_type, + operands, operand_count, + handler->user_data)) + return; + + session->handlers = g_slist_remove(session->handlers, handler); + g_free(handler); + + return; + } +} + +static gboolean session_cb(GIOChannel *chan, GIOCondition cond, + gpointer data) +{ + struct avctp *session = data; + uint8_t buf[1024], *operands, code, subunit; + struct avctp_header *avctp; + struct avc_header *avc; + int ret, packet_size, operand_count, sock; + struct avctp_pdu_handler *handler; + + if (cond & (G_IO_ERR | G_IO_HUP | G_IO_NVAL)) + goto failed; + + sock = g_io_channel_unix_get_fd(session->io); + + ret = read(sock, buf, sizeof(buf)); + if (ret <= 0) + goto failed; + + DBG("Got %d bytes of data for AVCTP session %p", ret, session); + + if ((unsigned int) ret < sizeof(struct avctp_header)) { + error("Too small AVCTP packet"); + goto failed; + } + + avctp = (struct avctp_header *) buf; + + DBG("AVCTP transaction %u, packet type %u, C/R %u, IPID %u, " + "PID 0x%04X", + avctp->transaction, avctp->packet_type, + avctp->cr, avctp->ipid, ntohs(avctp->pid)); + + ret -= sizeof(struct avctp_header); + if ((unsigned int) ret < sizeof(struct avc_header)) { + error("Too small AVCTP packet"); + goto failed; + } + + avc = (struct avc_header *) (buf + sizeof(struct avctp_header)); + + ret -= sizeof(struct avc_header); + + operands = buf + sizeof(struct avctp_header) + sizeof(struct avc_header); + operand_count = ret; + + DBG("AV/C %s 0x%01X, subunit_type 0x%02X, subunit_id 0x%01X, " + "opcode 0x%02X, %d operands", + avctp->cr ? "response" : "command", + avc->code, avc->subunit_type, avc->subunit_id, + avc->opcode, operand_count); + + if (avctp->cr == AVCTP_RESPONSE) { + handle_response(session, avctp, avc, operands, operand_count); + return TRUE; + } + + packet_size = AVCTP_HEADER_LENGTH + AVC_HEADER_LENGTH; + avctp->cr = AVCTP_RESPONSE; + + if (avctp->packet_type != AVCTP_PACKET_SINGLE) { + avc->code = AVC_CTYPE_NOT_IMPLEMENTED; + goto done; + } + + if (avctp->pid != htons(AV_REMOTE_SVCLASS_ID)) { + avctp->ipid = 1; + packet_size = AVCTP_HEADER_LENGTH; + goto done; + } + + handler = find_handler(handlers, avc->opcode); + if (!handler) { + DBG("handler not found for 0x%02x", avc->opcode); + packet_size += avrcp_handle_vendor_reject(&code, operands); + avc->code = code; + goto done; + } + + code = avc->code; + subunit = avc->subunit_type; + + packet_size += handler->cb(session, avctp->transaction, &code, + &subunit, operands, operand_count, + handler->user_data); + + avc->code = code; + avc->subunit_type = subunit; + +done: + ret = write(sock, buf, packet_size); + if (ret != packet_size) + goto failed; + + return TRUE; + +failed: + DBG("AVCTP session %p got disconnected", session); + avctp_set_state(session, AVCTP_STATE_DISCONNECTED); + return FALSE; +} + +static int uinput_create(char *name) +{ + struct uinput_dev dev; + int fd, err, i; + + fd = open("/dev/uinput", O_RDWR); + if (fd < 0) { + fd = open("/dev/input/uinput", O_RDWR); + if (fd < 0) { + fd = open("/dev/misc/uinput", O_RDWR); + if (fd < 0) { + err = -errno; + error("Can't open input device: %s (%d)", + strerror(-err), -err); + return err; + } + } + } + + memset(&dev, 0, sizeof(dev)); + if (name) + strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE - 1); + + dev.id.bustype = BUS_BLUETOOTH; + dev.id.vendor = 0x0000; + dev.id.product = 0x0000; + dev.id.version = 0x0000; + + if (write(fd, &dev, sizeof(dev)) < 0) { + err = -errno; + error("Can't write device information: %s (%d)", + strerror(-err), -err); + close(fd); + return err; + } + + ioctl(fd, UI_SET_EVBIT, EV_KEY); + ioctl(fd, UI_SET_EVBIT, EV_REL); + ioctl(fd, UI_SET_EVBIT, EV_REP); + ioctl(fd, UI_SET_EVBIT, EV_SYN); + + for (i = 0; key_map[i].name != NULL; i++) + ioctl(fd, UI_SET_KEYBIT, key_map[i].uinput); + + if (ioctl(fd, UI_DEV_CREATE, NULL) < 0) { + err = -errno; + error("Can't create uinput device: %s (%d)", + strerror(-err), -err); + close(fd); + return err; + } + + return fd; +} + +static void init_uinput(struct avctp *session) +{ + struct audio_device *dev; + char address[18], name[248 + 1]; + + dev = manager_get_device(&session->server->src, &session->dst, FALSE); + + device_get_name(dev->btd_dev, name, sizeof(name)); + if (g_str_equal(name, "Nokia CK-20W")) { + session->key_quirks[FORWARD_OP] |= QUIRK_NO_RELEASE; + session->key_quirks[BACKWARD_OP] |= QUIRK_NO_RELEASE; + session->key_quirks[PLAY_OP] |= QUIRK_NO_RELEASE; + session->key_quirks[PAUSE_OP] |= QUIRK_NO_RELEASE; + } + + ba2str(&session->dst, address); + + session->uinput = uinput_create(address); + if (session->uinput < 0) + error("AVRCP: failed to init uinput for %s", address); + else + DBG("AVRCP: uinput initialized for %s", address); +} + +static void avctp_connect_cb(GIOChannel *chan, GError *err, gpointer data) +{ + struct avctp *session = data; + char address[18]; + uint16_t imtu; + GError *gerr = NULL; + + if (err) { + avctp_set_state(session, AVCTP_STATE_DISCONNECTED); + error("%s", err->message); + return; + } + + bt_io_get(chan, BT_IO_L2CAP, &gerr, + BT_IO_OPT_DEST, &address, + BT_IO_OPT_IMTU, &imtu, + BT_IO_OPT_INVALID); + if (gerr) { + avctp_set_state(session, AVCTP_STATE_DISCONNECTED); + error("%s", gerr->message); + g_error_free(gerr); + return; + } + + DBG("AVCTP: connected to %s", address); + + if (!session->io) + session->io = g_io_channel_ref(chan); + + init_uinput(session); + + avctp_set_state(session, AVCTP_STATE_CONNECTED); + session->mtu = imtu; + session->io_id = g_io_add_watch(chan, + G_IO_IN | G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) session_cb, session); +} + +static void auth_cb(DBusError *derr, void *user_data) +{ + struct avctp *session = user_data; + GError *err = NULL; + + if (session->io_id) { + g_source_remove(session->io_id); + session->io_id = 0; + } + + if (derr && dbus_error_is_set(derr)) { + error("Access denied: %s", derr->message); + avctp_set_state(session, AVCTP_STATE_DISCONNECTED); + return; + } + + if (!bt_io_accept(session->io, avctp_connect_cb, session, + NULL, &err)) { + error("bt_io_accept: %s", err->message); + g_error_free(err); + avctp_set_state(session, AVCTP_STATE_DISCONNECTED); + } +} + +static struct avctp_server *find_server(GSList *list, const bdaddr_t *src) +{ + for (; list; list = list->next) { + struct avctp_server *server = list->data; + + if (bacmp(&server->src, src) == 0) + return server; + } + + return NULL; +} + +static struct avctp *find_session(GSList *list, const bdaddr_t *dst) +{ + for (; list != NULL; list = g_slist_next(list)) { + struct avctp *s = list->data; + + if (bacmp(dst, &s->dst)) + continue; + + return s; + } + + return NULL; +} + +static struct avctp *avctp_get_internal(const bdaddr_t *src, + const bdaddr_t *dst) +{ + struct avctp_server *server; + struct avctp *session; + + assert(src != NULL); + assert(dst != NULL); + + server = find_server(servers, src); + if (server == NULL) + return NULL; + + session = find_session(server->sessions, dst); + if (session) + return session; + + session = g_new0(struct avctp, 1); + + session->server = server; + bacpy(&session->dst, dst); + session->state = AVCTP_STATE_DISCONNECTED; + + server->sessions = g_slist_append(server->sessions, session); + + return session; +} + +static void avctp_confirm_cb(GIOChannel *chan, gpointer data) +{ + struct avctp *session; + struct audio_device *dev; + char address[18]; + bdaddr_t src, dst; + GError *err = NULL; + + bt_io_get(chan, BT_IO_L2CAP, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_DEST_BDADDR, &dst, + BT_IO_OPT_DEST, address, + BT_IO_OPT_INVALID); + if (err) { + error("%s", err->message); + g_error_free(err); + g_io_channel_shutdown(chan, TRUE, NULL); + return; + } + + DBG("AVCTP: incoming connect from %s", address); + + session = avctp_get_internal(&src, &dst); + if (!session) + goto drop; + + dev = manager_get_device(&src, &dst, FALSE); + if (!dev) { + dev = manager_get_device(&src, &dst, TRUE); + if (!dev) { + error("Unable to get audio device object for %s", + address); + goto drop; + } + } + + if (dev->control == NULL) { + btd_device_add_uuid(dev->btd_dev, AVRCP_REMOTE_UUID); + if (dev->control == NULL) + goto drop; + } + + if (session->io) { + error("Refusing unexpected connect from %s", address); + goto drop; + } + + avctp_set_state(session, AVCTP_STATE_CONNECTING); + session->io = g_io_channel_ref(chan); + + if (audio_device_request_authorization(dev, AVRCP_TARGET_UUID, + auth_cb, session) < 0) + goto drop; + + session->io_id = g_io_add_watch(chan, G_IO_ERR | G_IO_HUP | G_IO_NVAL, + session_cb, session); + return; + +drop: + if (!session || !session->io) + g_io_channel_shutdown(chan, TRUE, NULL); + if (session) + avctp_set_state(session, AVCTP_STATE_DISCONNECTED); +} + +static GIOChannel *avctp_server_socket(const bdaddr_t *src, gboolean master) +{ + GError *err = NULL; + GIOChannel *io; + + io = bt_io_listen(BT_IO_L2CAP, NULL, avctp_confirm_cb, NULL, + NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, src, + BT_IO_OPT_PSM, AVCTP_PSM, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_MASTER, master, + BT_IO_OPT_INVALID); + if (!io) { + error("%s", err->message); + g_error_free(err); + } + + return io; +} + +static unsigned int passthrough_id = 0; +static unsigned int unit_id = 0; +static unsigned int subunit_id = 0; + +int avctp_register(const bdaddr_t *src, gboolean master) +{ + struct avctp_server *server; + + server = g_new0(struct avctp_server, 1); + if (!server) + return -ENOMEM; + + server->io = avctp_server_socket(src, master); + if (!server->io) { + g_free(server); + return -1; + } + + bacpy(&server->src, src); + + servers = g_slist_append(servers, server); + + if (!passthrough_id) + passthrough_id = avctp_register_pdu_handler(AVC_OP_PASSTHROUGH, + handle_panel_passthrough, NULL); + + if (!unit_id) + unit_id = avctp_register_pdu_handler(AVC_OP_UNITINFO, handle_unit_info, + NULL); + + if (!subunit_id) + subunit_id = avctp_register_pdu_handler(AVC_OP_SUBUNITINFO, + handle_subunit_info, NULL); + + return 0; +} + +void avctp_unregister(const bdaddr_t *src) +{ + struct avctp_server *server; + + server = find_server(servers, src); + if (!server) + return; + + while (server->sessions) + avctp_disconnected(server->sessions->data); + + servers = g_slist_remove(servers, server); + + g_io_channel_shutdown(server->io, TRUE, NULL); + g_io_channel_unref(server->io); + g_free(server); + + if (servers) + return; + + if (passthrough_id) { + avctp_unregister_pdu_handler(passthrough_id); + passthrough_id = 0; + } + + if (unit_id) { + avctp_unregister_pdu_handler(unit_id); + passthrough_id = 0; + } + + if (subunit_id) { + avctp_unregister_pdu_handler(subunit_id); + subunit_id = 0; + } +} + +int avctp_send_passthrough(struct avctp *session, uint8_t op) +{ + unsigned char buf[AVCTP_HEADER_LENGTH + AVC_HEADER_LENGTH + 2]; + struct avctp_header *avctp = (void *) buf; + struct avc_header *avc = (void *) &buf[AVCTP_HEADER_LENGTH]; + uint8_t *operands = &buf[AVCTP_HEADER_LENGTH + AVC_HEADER_LENGTH]; + int sk; + + if (session->state != AVCTP_STATE_CONNECTED) + return -ENOTCONN; + + memset(buf, 0, sizeof(buf)); + + avctp->transaction = id++; + avctp->packet_type = AVCTP_PACKET_SINGLE; + avctp->cr = AVCTP_COMMAND; + avctp->pid = htons(AV_REMOTE_SVCLASS_ID); + + avc->code = AVC_CTYPE_CONTROL; + avc->subunit_type = AVC_SUBUNIT_PANEL; + avc->opcode = AVC_OP_PASSTHROUGH; + + operands[0] = op & 0x7f; + operands[1] = 0; + + sk = g_io_channel_unix_get_fd(session->io); + + if (write(sk, buf, sizeof(buf)) < 0) + return -errno; + + /* Button release */ + avctp->transaction = id++; + operands[0] |= 0x80; + + if (write(sk, buf, sizeof(buf)) < 0) + return -errno; + + return 0; +} + +static int avctp_send(struct avctp *session, uint8_t transaction, uint8_t cr, + uint8_t code, uint8_t subunit, uint8_t opcode, + uint8_t *operands, size_t operand_count) +{ + uint8_t *buf; + struct avctp_header *avctp; + struct avc_header *avc; + uint8_t *pdu; + int sk, err = 0; + uint16_t size; + + if (session->state != AVCTP_STATE_CONNECTED) + return -ENOTCONN; + + sk = g_io_channel_unix_get_fd(session->io); + size = AVCTP_HEADER_LENGTH + AVC_HEADER_LENGTH + operand_count; + buf = g_malloc0(size); + + avctp = (void *) buf; + avc = (void *) &buf[AVCTP_HEADER_LENGTH]; + pdu = (void *) &buf[AVCTP_HEADER_LENGTH + AVC_HEADER_LENGTH]; + + avctp->transaction = transaction; + avctp->packet_type = AVCTP_PACKET_SINGLE; + avctp->cr = cr; + avctp->pid = htons(AV_REMOTE_SVCLASS_ID); + + avc->code = code; + avc->subunit_type = subunit; + avc->opcode = opcode; + + memcpy(pdu, operands, operand_count); + + if (write(sk, buf, size) < 0) + err = -errno; + + g_free(buf); + return err; +} + +int avctp_send_vendordep(struct avctp *session, uint8_t transaction, + uint8_t code, uint8_t subunit, + uint8_t *operands, size_t operand_count) +{ + return avctp_send(session, transaction, AVCTP_RESPONSE, code, subunit, + AVC_OP_VENDORDEP, operands, operand_count); +} + +int avctp_send_vendordep_req(struct avctp *session, uint8_t code, + uint8_t subunit, uint8_t *operands, + size_t operand_count, + avctp_rsp_cb func, void *user_data) +{ + struct avctp_rsp_handler *handler; + int err; + + err = avctp_send(session, id, AVCTP_COMMAND, code, subunit, + AVC_OP_VENDORDEP, operands, operand_count); + if (err < 0) + return err; + + handler = g_new0(struct avctp_rsp_handler, 1); + handler->id = id; + handler->func = func; + handler->user_data = user_data; + + session->handlers = g_slist_prepend(session->handlers, handler); + + id++; + + return 0; +} + +unsigned int avctp_add_state_cb(avctp_state_cb cb, void *user_data) +{ + struct avctp_state_callback *state_cb; + static unsigned int id = 0; + + state_cb = g_new(struct avctp_state_callback, 1); + state_cb->cb = cb; + state_cb->user_data = user_data; + state_cb->id = ++id; + + callbacks = g_slist_append(callbacks, state_cb); + + return state_cb->id; +} + +gboolean avctp_remove_state_cb(unsigned int id) +{ + GSList *l; + + for (l = callbacks; l != NULL; l = l->next) { + struct avctp_state_callback *cb = l->data; + if (cb && cb->id == id) { + callbacks = g_slist_remove(callbacks, cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} + +unsigned int avctp_register_pdu_handler(uint8_t opcode, avctp_pdu_cb cb, + void *user_data) +{ + struct avctp_pdu_handler *handler; + static unsigned int id = 0; + + handler = find_handler(handlers, opcode); + if (handler) + return 0; + + handler = g_new(struct avctp_pdu_handler, 1); + handler->opcode = opcode; + handler->cb = cb; + handler->user_data = user_data; + handler->id = ++id; + + handlers = g_slist_append(handlers, handler); + + return handler->id; +} + +gboolean avctp_unregister_pdu_handler(unsigned int id) +{ + GSList *l; + + for (l = handlers; l != NULL; l = l->next) { + struct avctp_pdu_handler *handler = l->data; + + if (handler->id == id) { + handlers = g_slist_remove(handlers, handler); + g_free(handler); + return TRUE; + } + } + + return FALSE; +} + +struct avctp *avctp_connect(const bdaddr_t *src, const bdaddr_t *dst) +{ + struct avctp *session; + GError *err = NULL; + GIOChannel *io; + + session = avctp_get_internal(src, dst); + if (!session) + return NULL; + + if (session->state > AVCTP_STATE_DISCONNECTED) + return session; + + avctp_set_state(session, AVCTP_STATE_CONNECTING); + + io = bt_io_connect(BT_IO_L2CAP, avctp_connect_cb, session, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &session->server->src, + BT_IO_OPT_DEST_BDADDR, &session->dst, + BT_IO_OPT_PSM, AVCTP_PSM, + BT_IO_OPT_INVALID); + if (err) { + avctp_set_state(session, AVCTP_STATE_DISCONNECTED); + error("%s", err->message); + g_error_free(err); + return NULL; + } + + session->io = io; + + return session; +} + +void avctp_disconnect(struct avctp *session) +{ + if (!session->io) + return; + + avctp_set_state(session, AVCTP_STATE_DISCONNECTED); +} + +struct avctp *avctp_get(const bdaddr_t *src, const bdaddr_t *dst) +{ + return avctp_get_internal(src, dst); +} diff --git a/audio/avctp.h b/audio/avctp.h new file mode 100644 index 0000000..d0cbd97 --- /dev/null +++ b/audio/avctp.h @@ -0,0 +1,106 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define AVCTP_PSM 23 + +#define AVC_MTU 512 +#define AVC_HEADER_LENGTH 3 + +/* ctype entries */ +#define AVC_CTYPE_CONTROL 0x0 +#define AVC_CTYPE_STATUS 0x1 +#define AVC_CTYPE_NOTIFY 0x3 +#define AVC_CTYPE_NOT_IMPLEMENTED 0x8 +#define AVC_CTYPE_ACCEPTED 0x9 +#define AVC_CTYPE_REJECTED 0xA +#define AVC_CTYPE_STABLE 0xC +#define AVC_CTYPE_CHANGED 0xD +#define AVC_CTYPE_INTERIM 0xF + +/* opcodes */ +#define AVC_OP_VENDORDEP 0x00 +#define AVC_OP_UNITINFO 0x30 +#define AVC_OP_SUBUNITINFO 0x31 +#define AVC_OP_PASSTHROUGH 0x7c + +/* subunits of interest */ +#define AVC_SUBUNIT_PANEL 0x09 + +/* operands in passthrough commands */ +#define VOL_UP_OP 0x41 +#define VOL_DOWN_OP 0x42 +#define MUTE_OP 0x43 +#define PLAY_OP 0x44 +#define STAVC_OP_OP 0x45 +#define PAUSE_OP 0x46 +#define RECORD_OP 0x47 +#define REWIND_OP 0x48 +#define FAST_FORWARD_OP 0x49 +#define EJECT_OP 0x4a +#define FORWARD_OP 0x4b +#define BACKWARD_OP 0x4c + +struct avctp; + +typedef enum { + AVCTP_STATE_DISCONNECTED = 0, + AVCTP_STATE_CONNECTING, + AVCTP_STATE_CONNECTED +} avctp_state_t; + +typedef void (*avctp_state_cb) (struct audio_device *dev, + avctp_state_t old_state, + avctp_state_t new_state, + void *user_data); + +typedef size_t (*avctp_pdu_cb) (struct avctp *session, uint8_t transaction, + uint8_t *code, uint8_t *subunit, + uint8_t *operands, size_t operand_count, + void *user_data); +typedef gboolean (*avctp_rsp_cb) (struct avctp *session, uint8_t code, + uint8_t subunit, uint8_t *operands, + size_t operand_count, void *user_data); + +unsigned int avctp_add_state_cb(avctp_state_cb cb, void *user_data); +gboolean avctp_remove_state_cb(unsigned int id); + +int avctp_register(const bdaddr_t *src, gboolean master); +void avctp_unregister(const bdaddr_t *src); + +struct avctp *avctp_connect(const bdaddr_t *src, const bdaddr_t *dst); +struct avctp *avctp_get(const bdaddr_t *src, const bdaddr_t *dst); +void avctp_disconnect(struct avctp *session); + +unsigned int avctp_register_pdu_handler(uint8_t opcode, avctp_pdu_cb cb, + void *user_data); +gboolean avctp_unregister_pdu_handler(unsigned int id); + +int avctp_send_passthrough(struct avctp *session, uint8_t op); +int avctp_send_vendordep(struct avctp *session, uint8_t transaction, + uint8_t code, uint8_t subunit, + uint8_t *operands, size_t operand_count); +int avctp_send_vendordep_req(struct avctp *session, uint8_t code, + uint8_t subunit, uint8_t *operands, + size_t operand_count, + avctp_rsp_cb func, void *user_data); diff --git a/audio/avdtp.c b/audio/avdtp.c new file mode 100644 index 0000000..b2270f7 --- /dev/null +++ b/audio/avdtp.c @@ -0,0 +1,4051 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "log.h" + +#include "../src/adapter.h" +#include "../src/manager.h" +#include "../src/device.h" + +#include "device.h" +#include "manager.h" +#include "control.h" +#include "avdtp.h" +#include "btio.h" +#include "sink.h" +#include "source.h" + +#define AVDTP_PSM 25 + +#define MAX_SEID 0x3E + +#ifndef MAX +# define MAX(x, y) ((x) > (y) ? (x) : (y)) +#endif + +#define AVDTP_DISCOVER 0x01 +#define AVDTP_GET_CAPABILITIES 0x02 +#define AVDTP_SET_CONFIGURATION 0x03 +#define AVDTP_GET_CONFIGURATION 0x04 +#define AVDTP_RECONFIGURE 0x05 +#define AVDTP_OPEN 0x06 +#define AVDTP_START 0x07 +#define AVDTP_CLOSE 0x08 +#define AVDTP_SUSPEND 0x09 +#define AVDTP_ABORT 0x0A +#define AVDTP_SECURITY_CONTROL 0x0B +#define AVDTP_GET_ALL_CAPABILITIES 0x0C +#define AVDTP_DELAY_REPORT 0x0D + +#define AVDTP_PKT_TYPE_SINGLE 0x00 +#define AVDTP_PKT_TYPE_START 0x01 +#define AVDTP_PKT_TYPE_CONTINUE 0x02 +#define AVDTP_PKT_TYPE_END 0x03 + +#define AVDTP_MSG_TYPE_COMMAND 0x00 +#define AVDTP_MSG_TYPE_GEN_REJECT 0x01 +#define AVDTP_MSG_TYPE_ACCEPT 0x02 +#define AVDTP_MSG_TYPE_REJECT 0x03 + +#ifdef __TIZEN_PATCH__ +#define REQ_TIMEOUT 10 +#else +#define REQ_TIMEOUT 6 +#endif +#define ABORT_TIMEOUT 2 +#define DISCONNECT_TIMEOUT 1 +#define STREAM_TIMEOUT 20 +#define START_TIMEOUT 1 + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct avdtp_common_header { + uint8_t message_type:2; + uint8_t packet_type:2; + uint8_t transaction:4; +} __attribute__ ((packed)); + +struct avdtp_single_header { + uint8_t message_type:2; + uint8_t packet_type:2; + uint8_t transaction:4; + uint8_t signal_id:6; + uint8_t rfa0:2; +} __attribute__ ((packed)); + +struct avdtp_start_header { + uint8_t message_type:2; + uint8_t packet_type:2; + uint8_t transaction:4; + uint8_t no_of_packets; + uint8_t signal_id:6; + uint8_t rfa0:2; +} __attribute__ ((packed)); + +struct avdtp_continue_header { + uint8_t message_type:2; + uint8_t packet_type:2; + uint8_t transaction:4; +} __attribute__ ((packed)); + +struct seid_info { + uint8_t rfa0:1; + uint8_t inuse:1; + uint8_t seid:6; + uint8_t rfa2:3; + uint8_t type:1; + uint8_t media_type:4; +} __attribute__ ((packed)); + +struct seid { + uint8_t rfa0:2; + uint8_t seid:6; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct avdtp_common_header { + uint8_t transaction:4; + uint8_t packet_type:2; + uint8_t message_type:2; +} __attribute__ ((packed)); + +struct avdtp_single_header { + uint8_t transaction:4; + uint8_t packet_type:2; + uint8_t message_type:2; + uint8_t rfa0:2; + uint8_t signal_id:6; +} __attribute__ ((packed)); + +struct avdtp_start_header { + uint8_t transaction:4; + uint8_t packet_type:2; + uint8_t message_type:2; + uint8_t no_of_packets; + uint8_t rfa0:2; + uint8_t signal_id:6; +} __attribute__ ((packed)); + +struct avdtp_continue_header { + uint8_t transaction:4; + uint8_t packet_type:2; + uint8_t message_type:2; +} __attribute__ ((packed)); + +struct seid_info { + uint8_t seid:6; + uint8_t inuse:1; + uint8_t rfa0:1; + uint8_t media_type:4; + uint8_t type:1; + uint8_t rfa2:3; +} __attribute__ ((packed)); + +struct seid { + uint8_t seid:6; + uint8_t rfa0:2; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif + +/* packets */ + +struct discover_resp { + struct seid_info seps[0]; +} __attribute__ ((packed)); + +struct getcap_resp { + uint8_t caps[0]; +} __attribute__ ((packed)); + +struct start_req { + struct seid first_seid; + struct seid other_seids[0]; +} __attribute__ ((packed)); + +struct suspend_req { + struct seid first_seid; + struct seid other_seids[0]; +} __attribute__ ((packed)); + +struct seid_rej { + uint8_t error; +} __attribute__ ((packed)); + +struct conf_rej { + uint8_t category; + uint8_t error; +} __attribute__ ((packed)); + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct seid_req { + uint8_t rfa0:2; + uint8_t acp_seid:6; +} __attribute__ ((packed)); + +struct setconf_req { + uint8_t rfa0:2; + uint8_t acp_seid:6; + uint8_t rfa1:2; + uint8_t int_seid:6; + + uint8_t caps[0]; +} __attribute__ ((packed)); + +struct stream_rej { + uint8_t rfa0:2; + uint8_t acp_seid:6; + uint8_t error; +} __attribute__ ((packed)); + +struct reconf_req { + uint8_t rfa0:2; + uint8_t acp_seid:6; + + uint8_t serv_cap; + uint8_t serv_cap_len; + + uint8_t caps[0]; +} __attribute__ ((packed)); + +struct delay_req { + uint8_t rfa0:2; + uint8_t acp_seid:6; + uint16_t delay; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct seid_req { + uint8_t acp_seid:6; + uint8_t rfa0:2; +} __attribute__ ((packed)); + +struct setconf_req { + uint8_t acp_seid:6; + uint8_t rfa0:2; + uint8_t int_seid:6; + uint8_t rfa1:2; + + uint8_t caps[0]; +} __attribute__ ((packed)); + +struct stream_rej { + uint8_t acp_seid:6; + uint8_t rfa0:2; + uint8_t error; +} __attribute__ ((packed)); + +struct reconf_req { + uint8_t acp_seid:6; + uint8_t rfa0:2; + + uint8_t serv_cap; + uint8_t serv_cap_len; + + uint8_t caps[0]; +} __attribute__ ((packed)); + +struct delay_req { + uint8_t acp_seid:6; + uint8_t rfa0:2; + uint16_t delay; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif + +struct in_buf { + gboolean active; + int no_of_packets; + uint8_t transaction; + uint8_t message_type; + uint8_t signal_id; + uint8_t buf[1024]; + uint8_t data_size; +}; + +struct pending_req { + uint8_t transaction; + uint8_t signal_id; + void *data; + size_t data_size; + struct avdtp_stream *stream; /* Set if the request targeted a stream */ + guint timeout; + gboolean collided; +}; + +struct avdtp_remote_sep { + uint8_t seid; + uint8_t type; + uint8_t media_type; + struct avdtp_service_capability *codec; + gboolean delay_reporting; + GSList *caps; /* of type struct avdtp_service_capability */ + struct avdtp_stream *stream; +}; + +struct avdtp_server { + bdaddr_t src; + uint16_t version; + GIOChannel *io; + GSList *seps; + GSList *sessions; +}; + +struct avdtp_local_sep { + avdtp_state_t state; + struct avdtp_stream *stream; + struct seid_info info; + uint8_t codec; + gboolean delay_reporting; + GSList *caps; + struct avdtp_sep_ind *ind; + struct avdtp_sep_cfm *cfm; + void *user_data; + struct avdtp_server *server; +}; + +struct stream_callback { + avdtp_stream_state_cb cb; + void *user_data; + unsigned int id; +}; + +struct avdtp_state_callback { + avdtp_session_state_cb cb; + void *user_data; + unsigned int id; +}; + +struct avdtp_stream { + GIOChannel *io; + uint16_t imtu; + uint16_t omtu; + struct avdtp *session; + struct avdtp_local_sep *lsep; + uint8_t rseid; + GSList *caps; + GSList *callbacks; + struct avdtp_service_capability *codec; + guint io_id; /* Transport GSource ID */ + guint timer; /* Waiting for other side to close or open + * the transport channel */ + gboolean open_acp; /* If we are in ACT role for Open */ + gboolean close_int; /* If we are in INT role for Close */ + gboolean abort_int; /* If we are in INT role for Abort */ + guint start_timer; /* Wait START command timer */ + gboolean delay_reporting; + uint16_t delay; /* AVDTP 1.3 Delay Reporting feature */ + gboolean starting; /* only valid while sep state == OPEN */ +}; + +/* Structure describing an AVDTP connection between two devices */ + +struct avdtp { + int ref; + int free_lock; + + uint16_t version; + + struct avdtp_server *server; + bdaddr_t dst; + + avdtp_session_state_t state; + + /* True if the entire device is being disconnected */ + gboolean device_disconnect; + + GIOChannel *io; + guint io_id; + + GSList *seps; /* Elements of type struct avdtp_remote_sep * */ + + GSList *streams; /* Elements of type struct avdtp_stream * */ + + GSList *req_queue; /* Elements of type struct pending_req * */ + GSList *prio_queue; /* Same as req_queue but is processed before it */ + + struct avdtp_stream *pending_open; + + uint16_t imtu; + uint16_t omtu; + + struct in_buf in; + + char *buf; + + avdtp_discover_cb_t discov_cb; + void *user_data; + + struct pending_req *req; + + guint dc_timer; + + /* Attempt stream setup instead of disconnecting */ + gboolean stream_setup; + + DBusPendingCall *pending_auth; +}; + +static GSList *servers = NULL; + +static GSList *avdtp_callbacks = NULL; + +static gboolean auto_connect = TRUE; + +static int send_request(struct avdtp *session, gboolean priority, + struct avdtp_stream *stream, uint8_t signal_id, + void *buffer, size_t size); +static gboolean avdtp_parse_resp(struct avdtp *session, + struct avdtp_stream *stream, + uint8_t transaction, uint8_t signal_id, + void *buf, int size); +static gboolean avdtp_parse_rej(struct avdtp *session, + struct avdtp_stream *stream, + uint8_t transaction, uint8_t signal_id, + void *buf, int size); +static int process_queue(struct avdtp *session); +static void connection_lost(struct avdtp *session, int err); +static void avdtp_sep_set_state(struct avdtp *session, + struct avdtp_local_sep *sep, + avdtp_state_t state); +static void auth_cb(DBusError *derr, void *user_data); + +static struct avdtp_server *find_server(GSList *list, const bdaddr_t *src) +{ + for (; list; list = list->next) { + struct avdtp_server *server = list->data; + + if (bacmp(&server->src, src) == 0) + return server; + } + + return NULL; +} + +static const char *avdtp_statestr(avdtp_state_t state) +{ + switch (state) { + case AVDTP_STATE_IDLE: + return "IDLE"; + case AVDTP_STATE_CONFIGURED: + return "CONFIGURED"; + case AVDTP_STATE_OPEN: + return "OPEN"; + case AVDTP_STATE_STREAMING: + return "STREAMING"; + case AVDTP_STATE_CLOSING: + return "CLOSING"; + case AVDTP_STATE_ABORTING: + return "ABORTING"; + default: + return ""; + } +} + +static gboolean try_send(int sk, void *data, size_t len) +{ + int err; + + do { + err = send(sk, data, len, 0); + } while (err < 0 && errno == EINTR); + + if (err < 0) { + error("send: %s (%d)", strerror(errno), errno); + return FALSE; + } else if ((size_t) err != len) { + error("try_send: complete buffer not sent (%d/%zu bytes)", + err, len); + return FALSE; + } + + return TRUE; +} + +static gboolean avdtp_send(struct avdtp *session, uint8_t transaction, + uint8_t message_type, uint8_t signal_id, + void *data, size_t len) +{ + unsigned int cont_fragments, sent; + struct avdtp_start_header start; + struct avdtp_continue_header cont; + int sock; + + if (session->io == NULL) { + error("avdtp_send: session is closed"); + return FALSE; + } + + sock = g_io_channel_unix_get_fd(session->io); + + /* Single packet - no fragmentation */ + if (sizeof(struct avdtp_single_header) + len <= session->omtu) { + struct avdtp_single_header single; + + memset(&single, 0, sizeof(single)); + + single.transaction = transaction; + single.packet_type = AVDTP_PKT_TYPE_SINGLE; + single.message_type = message_type; + single.signal_id = signal_id; + + memcpy(session->buf, &single, sizeof(single)); + memcpy(session->buf + sizeof(single), data, len); + + return try_send(sock, session->buf, sizeof(single) + len); + } + + /* Check if there is enough space to start packet */ + if (session->omtu < sizeof(start)) { + error("No enough space to fragment packet"); + return FALSE; + } + + /* Count the number of needed fragments */ + cont_fragments = (len - (session->omtu - sizeof(start))) / + (session->omtu - sizeof(cont)) + 1; + + DBG("%zu bytes split into %d fragments", len, cont_fragments + 1); + + /* Send the start packet */ + memset(&start, 0, sizeof(start)); + start.transaction = transaction; + start.packet_type = AVDTP_PKT_TYPE_START; + start.message_type = message_type; + start.no_of_packets = cont_fragments + 1; + start.signal_id = signal_id; + + memcpy(session->buf, &start, sizeof(start)); + memcpy(session->buf + sizeof(start), data, + session->omtu - sizeof(start)); + + if (!try_send(sock, session->buf, session->omtu)) + return FALSE; + + DBG("first packet with %zu bytes sent", session->omtu - sizeof(start)); + + sent = session->omtu - sizeof(start); + + /* Send the continue fragments and the end packet */ + while (sent < len) { + int left, to_copy; + + left = len - sent; + if (left + sizeof(cont) > session->omtu) { + cont.packet_type = AVDTP_PKT_TYPE_CONTINUE; + to_copy = session->omtu - sizeof(cont); + DBG("sending continue with %d bytes", to_copy); + } else { + cont.packet_type = AVDTP_PKT_TYPE_END; + to_copy = left; + DBG("sending end with %d bytes", to_copy); + } + + cont.transaction = transaction; + cont.message_type = message_type; + + memcpy(session->buf, &cont, sizeof(cont)); + memcpy(session->buf + sizeof(cont), data + sent, to_copy); + + if (!try_send(sock, session->buf, to_copy + sizeof(cont))) + return FALSE; + + sent += to_copy; + } + + return TRUE; +} + +static void pending_req_free(struct pending_req *req) +{ + if (req->timeout) + g_source_remove(req->timeout); + g_free(req->data); + g_free(req); +} + +static void close_stream(struct avdtp_stream *stream) +{ + int sock; + + if (stream->io == NULL) + return; + + sock = g_io_channel_unix_get_fd(stream->io); + + shutdown(sock, SHUT_RDWR); + + g_io_channel_shutdown(stream->io, FALSE, NULL); + + g_io_channel_unref(stream->io); + stream->io = NULL; +} + +static gboolean stream_close_timeout(gpointer user_data) +{ + struct avdtp_stream *stream = user_data; + + DBG("Timed out waiting for peer to close the transport channel"); + + stream->timer = 0; + + close_stream(stream); + + return FALSE; +} + +static gboolean stream_open_timeout(gpointer user_data) +{ + struct avdtp_stream *stream = user_data; + + DBG("Timed out waiting for peer to open the transport channel"); + + stream->timer = 0; + + stream->session->pending_open = NULL; + + avdtp_abort(stream->session, stream); + + return FALSE; +} + +static gboolean disconnect_timeout(gpointer user_data) +{ + struct avdtp *session = user_data; + struct audio_device *dev; + gboolean stream_setup; + + session->dc_timer = 0; + stream_setup = session->stream_setup; + session->stream_setup = FALSE; + + dev = manager_get_device(&session->server->src, &session->dst, FALSE); + + if (dev && dev->sink && stream_setup) + sink_setup_stream(dev->sink, session); + else if (dev && dev->source && stream_setup) + source_setup_stream(dev->source, session); + else + connection_lost(session, ETIMEDOUT); + + return FALSE; +} + +static void remove_disconnect_timer(struct avdtp *session) +{ + g_source_remove(session->dc_timer); + session->dc_timer = 0; + session->stream_setup = FALSE; +} + +static void set_disconnect_timer(struct avdtp *session) +{ + if (session->dc_timer) + remove_disconnect_timer(session); + + if (session->device_disconnect) { + session->dc_timer = g_idle_add(disconnect_timeout, session); + return; + } + + session->dc_timer = g_timeout_add_seconds(DISCONNECT_TIMEOUT, + disconnect_timeout, + session); +} + +void avdtp_error_init(struct avdtp_error *err, uint8_t category, int id) +{ + err->category = category; + + if (category == AVDTP_ERRNO) + err->err.posix_errno = id; + else + err->err.error_code = id; +} + +uint8_t avdtp_error_category(struct avdtp_error *err) +{ + return err->category; +} + +int avdtp_error_error_code(struct avdtp_error *err) +{ + assert(err->category != AVDTP_ERRNO); + return err->err.error_code; +} + +int avdtp_error_posix_errno(struct avdtp_error *err) +{ + assert(err->category == AVDTP_ERRNO); + return err->err.posix_errno; +} + +static struct avdtp_stream *find_stream_by_rseid(struct avdtp *session, + uint8_t rseid) +{ + GSList *l; + + for (l = session->streams; l != NULL; l = g_slist_next(l)) { + struct avdtp_stream *stream = l->data; + + if (stream->rseid == rseid) + return stream; + } + + return NULL; +} + +static struct avdtp_remote_sep *find_remote_sep(GSList *seps, uint8_t seid) +{ + GSList *l; + + for (l = seps; l != NULL; l = g_slist_next(l)) { + struct avdtp_remote_sep *sep = l->data; + + if (sep->seid == seid) + return sep; + } + + return NULL; +} + +static void avdtp_set_state(struct avdtp *session, + avdtp_session_state_t new_state) +{ + GSList *l; + struct audio_device *dev; + bdaddr_t src, dst; + avdtp_session_state_t old_state = session->state; + + session->state = new_state; + + avdtp_get_peers(session, &src, &dst); + dev = manager_get_device(&src, &dst, FALSE); + if (dev == NULL) { + error("avdtp_set_state(): no matching audio device"); + return; + } + + for (l = avdtp_callbacks; l != NULL; l = l->next) { + struct avdtp_state_callback *cb = l->data; + cb->cb(dev, session, old_state, new_state, cb->user_data); + } +} + +static void stream_free(struct avdtp_stream *stream) +{ + struct avdtp_remote_sep *rsep; + + stream->lsep->info.inuse = 0; + stream->lsep->stream = NULL; + + rsep = find_remote_sep(stream->session->seps, stream->rseid); + if (rsep) + rsep->stream = NULL; + + if (stream->timer) + g_source_remove(stream->timer); + + if (stream->io) + close_stream(stream); + + if (stream->io_id) + g_source_remove(stream->io_id); + + g_slist_free_full(stream->callbacks, g_free); + g_slist_free_full(stream->caps, g_free); + + g_free(stream); +} + +static gboolean transport_cb(GIOChannel *chan, GIOCondition cond, + gpointer data) +{ + struct avdtp_stream *stream = data; + struct avdtp_local_sep *sep = stream->lsep; + + if (stream->close_int && sep->cfm && sep->cfm->close) + sep->cfm->close(stream->session, sep, stream, NULL, + sep->user_data); + + if (!(cond & G_IO_NVAL)) + close_stream(stream); + + stream->io_id = 0; + + if (!stream->abort_int) + avdtp_sep_set_state(stream->session, sep, AVDTP_STATE_IDLE); + + return FALSE; +} + +static int get_send_buffer_size(int sk) +{ + int size; + socklen_t optlen = sizeof(size); + + if (getsockopt(sk, SOL_SOCKET, SO_SNDBUF, &size, &optlen) < 0) { + int err = -errno; + error("getsockopt(SO_SNDBUF) failed: %s (%d)", strerror(-err), + -err); + return err; + } + + /* + * Doubled value is returned by getsockopt since kernel uses that + * space for its own purposes (see man 7 socket, bookkeeping overhead + * for SO_SNDBUF). + */ + return size / 2; +} + +static int set_send_buffer_size(int sk, int size) +{ + socklen_t optlen = sizeof(size); + + if (setsockopt(sk, SOL_SOCKET, SO_SNDBUF, &size, optlen) < 0) { + int err = -errno; + error("setsockopt(SO_SNDBUF) failed: %s (%d)", strerror(-err), + -err); + return err; + } + + return 0; +} + +static void handle_transport_connect(struct avdtp *session, GIOChannel *io, + uint16_t imtu, uint16_t omtu) +{ + struct avdtp_stream *stream = session->pending_open; + struct avdtp_local_sep *sep = stream->lsep; + int sk, buf_size, min_buf_size; + GError *err = NULL; + + session->pending_open = NULL; + + if (stream->timer) { + g_source_remove(stream->timer); + stream->timer = 0; + } + + if (io == NULL) { + if (!stream->open_acp && sep->cfm && sep->cfm->open) { + struct avdtp_error err; + avdtp_error_init(&err, AVDTP_ERRNO, EIO); + sep->cfm->open(session, sep, NULL, &err, + sep->user_data); + } + return; + } + + if (stream->io == NULL) + stream->io = g_io_channel_ref(io); + + stream->omtu = omtu; + stream->imtu = imtu; + + /* Apply special settings only if local SEP is of type SRC */ + if (sep->info.type != AVDTP_SEP_TYPE_SOURCE) + goto proceed; + + bt_io_set(stream->io, BT_IO_L2CAP, &err, + BT_IO_OPT_FLUSHABLE, TRUE, + BT_IO_OPT_INVALID); + if (err != NULL) { + error("Enabling flushable packets failed: %s", err->message); + g_error_free(err); + } else + DBG("Flushable packets enabled"); + + sk = g_io_channel_unix_get_fd(stream->io); + buf_size = get_send_buffer_size(sk); + if (buf_size < 0) + goto proceed; + + DBG("sk %d, omtu %d, send buffer size %d", sk, omtu, buf_size); + min_buf_size = omtu * 2; + if (buf_size < min_buf_size) { + DBG("send buffer size to be increassed to %d", + min_buf_size); + set_send_buffer_size(sk, min_buf_size); + } + +proceed: + if (!stream->open_acp && sep->cfm && sep->cfm->open) + sep->cfm->open(session, sep, stream, NULL, sep->user_data); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_OPEN); + + stream->io_id = g_io_add_watch(io, G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) transport_cb, stream); +} + +static int pending_req_cmp(gconstpointer a, gconstpointer b) +{ + const struct pending_req *req = a; + const struct avdtp_stream *stream = b; + + if (req->stream == stream) + return 0; + + return -1; +} + +static void cleanup_queue(struct avdtp *session, struct avdtp_stream *stream) +{ + GSList *l; + struct pending_req *req; + + while ((l = g_slist_find_custom(session->prio_queue, stream, + pending_req_cmp))) { + req = l->data; + pending_req_free(req); + session->prio_queue = g_slist_remove(session->prio_queue, req); + } + + while ((l = g_slist_find_custom(session->req_queue, stream, + pending_req_cmp))) { + req = l->data; + pending_req_free(req); + session->req_queue = g_slist_remove(session->req_queue, req); + } +} + +static void handle_unanswered_req(struct avdtp *session, + struct avdtp_stream *stream) +{ + struct pending_req *req; + struct avdtp_local_sep *lsep; + struct avdtp_error err; + + if (session->req->signal_id == AVDTP_ABORT) { + /* Avoid freeing the Abort request here */ + DBG("handle_unanswered_req: Abort req, returning"); + session->req->stream = NULL; + return; + } + + req = session->req; + session->req = NULL; + + avdtp_error_init(&err, AVDTP_ERRNO, EIO); + + lsep = stream->lsep; + + switch (req->signal_id) { + case AVDTP_RECONFIGURE: + error("No reply to Reconfigure request"); + if (lsep && lsep->cfm && lsep->cfm->reconfigure) + lsep->cfm->reconfigure(session, lsep, stream, &err, + lsep->user_data); + break; + case AVDTP_OPEN: + error("No reply to Open request"); + if (lsep && lsep->cfm && lsep->cfm->open) + lsep->cfm->open(session, lsep, stream, &err, + lsep->user_data); + break; + case AVDTP_START: + error("No reply to Start request"); + if (lsep && lsep->cfm && lsep->cfm->start) + lsep->cfm->start(session, lsep, stream, &err, + lsep->user_data); + break; + case AVDTP_SUSPEND: + error("No reply to Suspend request"); + if (lsep && lsep->cfm && lsep->cfm->suspend) + lsep->cfm->suspend(session, lsep, stream, &err, + lsep->user_data); + break; + case AVDTP_CLOSE: + error("No reply to Close request"); + if (lsep && lsep->cfm && lsep->cfm->close) + lsep->cfm->close(session, lsep, stream, &err, + lsep->user_data); + break; + case AVDTP_SET_CONFIGURATION: + error("No reply to SetConfiguration request"); + if (lsep && lsep->cfm && lsep->cfm->set_configuration) + lsep->cfm->set_configuration(session, lsep, stream, + &err, lsep->user_data); + } + + pending_req_free(req); +} + +static void avdtp_sep_set_state(struct avdtp *session, + struct avdtp_local_sep *sep, + avdtp_state_t state) +{ + struct avdtp_stream *stream = sep->stream; + avdtp_state_t old_state; + struct avdtp_error err, *err_ptr = NULL; + GSList *l; + + if (!stream) { + error("Error changing sep state: stream not available"); + return; + } + + if (sep->state == state) { + avdtp_error_init(&err, AVDTP_ERRNO, EIO); + DBG("stream state change failed: %s", avdtp_strerror(&err)); + err_ptr = &err; + } else { + err_ptr = NULL; + DBG("stream state changed: %s -> %s", + avdtp_statestr(sep->state), + avdtp_statestr(state)); + } + + old_state = sep->state; + sep->state = state; + + switch (state) { + case AVDTP_STATE_CONFIGURED: + if (sep->info.type == AVDTP_SEP_TYPE_SINK) + avdtp_delay_report(session, stream, stream->delay); + break; + case AVDTP_STATE_OPEN: + stream->starting = FALSE; + break; + case AVDTP_STATE_STREAMING: + if (stream->start_timer) { + g_source_remove(stream->start_timer); + stream->start_timer = 0; + } + stream->open_acp = FALSE; + break; + case AVDTP_STATE_CLOSING: + case AVDTP_STATE_ABORTING: + if (stream->start_timer) { + g_source_remove(stream->start_timer); + stream->start_timer = 0; + } + break; + case AVDTP_STATE_IDLE: + if (stream->start_timer) { + g_source_remove(stream->start_timer); + stream->start_timer = 0; + } + if (session->pending_open == stream) + handle_transport_connect(session, NULL, 0, 0); + if (session->req && session->req->stream == stream) + handle_unanswered_req(session, stream); + /* Remove pending commands for this stream from the queue */ + cleanup_queue(session, stream); + break; + default: + break; + } + + l = stream->callbacks; + while (l != NULL) { + struct stream_callback *cb = l->data; + l = g_slist_next(l); + cb->cb(stream, old_state, state, err_ptr, cb->user_data); + } + + if (state == AVDTP_STATE_IDLE && + g_slist_find(session->streams, stream)) { + session->streams = g_slist_remove(session->streams, stream); + stream_free(stream); + } +} + +static void finalize_discovery(struct avdtp *session, int err) +{ + struct avdtp_error avdtp_err; + + avdtp_error_init(&avdtp_err, AVDTP_ERRNO, err); + + if (!session->discov_cb) + return; + + session->discov_cb(session, session->seps, + err ? &avdtp_err : NULL, + session->user_data); + + session->discov_cb = NULL; + session->user_data = NULL; +} + +static void release_stream(struct avdtp_stream *stream, struct avdtp *session) +{ + struct avdtp_local_sep *sep = stream->lsep; + + if (sep->cfm && sep->cfm->abort && + (sep->state != AVDTP_STATE_ABORTING || + stream->abort_int)) + sep->cfm->abort(session, sep, stream, NULL, sep->user_data); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_IDLE); +} + +static int avdtp_cancel_authorization(struct avdtp *session) +{ + struct audio_device *dev; + + if (session->state != AVDTP_SESSION_STATE_CONNECTING) + return 0; + + dev = manager_get_device(&session->server->src, &session->dst, FALSE); + if (dev == NULL) + return -ENODEV; + + return audio_device_cancel_authorization(dev, auth_cb, session); +} + +static void connection_lost(struct avdtp *session, int err) +{ + char address[18]; + + ba2str(&session->dst, address); + DBG("Disconnected from %s", address); + + if (err != EACCES) + avdtp_cancel_authorization(session); + + session->free_lock = 1; + + finalize_discovery(session, err); + + g_slist_foreach(session->streams, (GFunc) release_stream, session); + session->streams = NULL; + + session->free_lock = 0; + + if (session->io) { + g_io_channel_shutdown(session->io, FALSE, NULL); + g_io_channel_unref(session->io); + session->io = NULL; + } + + avdtp_set_state(session, AVDTP_SESSION_STATE_DISCONNECTED); + + if (session->io_id) { + g_source_remove(session->io_id); + session->io_id = 0; + } + + if (session->dc_timer) + remove_disconnect_timer(session); + + if (session->ref != 1) + error("connection_lost: ref count not 1 after all callbacks"); + else + avdtp_unref(session); +} + +void avdtp_unref(struct avdtp *session) +{ + struct avdtp_server *server; + + if (!session) + return; + + session->ref--; + + DBG("%p: ref=%d", session, session->ref); + + if (session->ref == 1) { + if (session->state == AVDTP_SESSION_STATE_CONNECTING && + session->io) { + avdtp_cancel_authorization(session); + g_io_channel_shutdown(session->io, TRUE, NULL); + g_io_channel_unref(session->io); + session->io = NULL; + avdtp_set_state(session, + AVDTP_SESSION_STATE_DISCONNECTED); + } + + if (session->io) + set_disconnect_timer(session); + else if (!session->free_lock) /* Drop the local ref if we + aren't connected */ + session->ref--; + } + + if (session->ref > 0) + return; + + server = session->server; + + DBG("%p: freeing session and removing from list", session); + + if (session->dc_timer) + remove_disconnect_timer(session); + + server->sessions = g_slist_remove(server->sessions, session); + + if (session->req) + pending_req_free(session->req); + + g_slist_free_full(session->seps, g_free); + + g_free(session->buf); + + g_free(session); +} + +struct avdtp *avdtp_ref(struct avdtp *session) +{ + session->ref++; + DBG("%p: ref=%d", session, session->ref); + if (session->dc_timer) + remove_disconnect_timer(session); + return session; +} + +static struct avdtp_local_sep *find_local_sep_by_seid(struct avdtp_server *server, + uint8_t seid) +{ + GSList *l; + + for (l = server->seps; l != NULL; l = g_slist_next(l)) { + struct avdtp_local_sep *sep = l->data; + + if (sep->info.seid == seid) + return sep; + } + + return NULL; +} + +struct avdtp_remote_sep *avdtp_find_remote_sep(struct avdtp *session, + struct avdtp_local_sep *lsep) +{ + GSList *l; + + if (lsep->info.inuse) + return NULL; + + for (l = session->seps; l != NULL; l = g_slist_next(l)) { + struct avdtp_remote_sep *sep = l->data; + struct avdtp_service_capability *cap; + struct avdtp_media_codec_capability *codec_data; + + /* Type must be different: source <-> sink */ + if (sep->type == lsep->info.type) + continue; + + if (sep->media_type != lsep->info.media_type) + continue; + + if (!sep->codec) + continue; + + cap = sep->codec; + codec_data = (void *) cap->data; + + if (codec_data->media_codec_type != lsep->codec) + continue; + + if (sep->stream == NULL) + return sep; + } + + return NULL; +} + +static GSList *caps_to_list(uint8_t *data, int size, + struct avdtp_service_capability **codec, + gboolean *delay_reporting) +{ + GSList *caps; + int processed; + + if (delay_reporting) + *delay_reporting = FALSE; + + for (processed = 0, caps = NULL; processed + 2 <= size;) { + struct avdtp_service_capability *cap; + uint8_t length, category; + + category = data[0]; + length = data[1]; + + if (processed + 2 + length > size) { + error("Invalid capability data in getcap resp"); + break; + } + + cap = g_malloc(sizeof(struct avdtp_service_capability) + + length); + memcpy(cap, data, 2 + length); + + processed += 2 + length; + data += 2 + length; + + caps = g_slist_append(caps, cap); + + if (category == AVDTP_MEDIA_CODEC && + length >= + sizeof(struct avdtp_media_codec_capability)) + *codec = cap; + else if (category == AVDTP_DELAY_REPORTING && delay_reporting) + *delay_reporting = TRUE; + } + + return caps; +} + +static gboolean avdtp_unknown_cmd(struct avdtp *session, uint8_t transaction, + uint8_t signal_id) +{ + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_GEN_REJECT, + signal_id, NULL, 0); +} + +static gboolean avdtp_discover_cmd(struct avdtp *session, uint8_t transaction, + void *buf, int size) +{ + GSList *l; + unsigned int rsp_size, sep_count, i; + struct seid_info *seps; + gboolean ret; + + sep_count = g_slist_length(session->server->seps); + + if (sep_count == 0) { + uint8_t err = AVDTP_NOT_SUPPORTED_COMMAND; + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_DISCOVER, &err, sizeof(err)); + } + + rsp_size = sep_count * sizeof(struct seid_info); + + seps = g_new0(struct seid_info, sep_count); + + for (l = session->server->seps, i = 0; l != NULL; l = l->next, i++) { + struct avdtp_local_sep *sep = l->data; + + memcpy(&seps[i], &sep->info, sizeof(struct seid_info)); + } + + ret = avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_DISCOVER, seps, rsp_size); + g_free(seps); + + return ret; +} + +static gboolean avdtp_getcap_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, unsigned int size, + gboolean get_all) +{ + GSList *l, *caps; + struct avdtp_local_sep *sep = NULL; + unsigned int rsp_size; + uint8_t err, buf[1024], *ptr = buf; + uint8_t cmd; + + cmd = get_all ? AVDTP_GET_ALL_CAPABILITIES : AVDTP_GET_CAPABILITIES; + + if (size < sizeof(struct seid_req)) { + err = AVDTP_BAD_LENGTH; + goto failed; + } + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + if (get_all && session->server->version < 0x0103) + return avdtp_unknown_cmd(session, transaction, cmd); + + if (!sep->ind->get_capability(session, sep, get_all, &caps, + &err, sep->user_data)) + goto failed; + + for (l = caps, rsp_size = 0; l != NULL; l = g_slist_next(l)) { + struct avdtp_service_capability *cap = l->data; + + if (rsp_size + cap->length + 2 > sizeof(buf)) + break; + + memcpy(ptr, cap, cap->length + 2); + rsp_size += cap->length + 2; + ptr += cap->length + 2; + + g_free(cap); + } + + g_slist_free(caps); + + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, cmd, + buf, rsp_size); + +failed: + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, cmd, + &err, sizeof(err)); +} + +static void setconf_cb(struct avdtp *session, struct avdtp_stream *stream, + struct avdtp_error *err) +{ + struct conf_rej rej; + struct avdtp_local_sep *sep; + + if (err != NULL) { + rej.error = AVDTP_UNSUPPORTED_CONFIGURATION; + rej.category = err->err.error_code; + avdtp_send(session, session->in.transaction, + AVDTP_MSG_TYPE_REJECT, AVDTP_SET_CONFIGURATION, + &rej, sizeof(rej)); + return; + } + + if (!avdtp_send(session, session->in.transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_SET_CONFIGURATION, NULL, 0)) { + stream_free(stream); + return; + } + + sep = stream->lsep; + sep->stream = stream; + sep->info.inuse = 1; + session->streams = g_slist_append(session->streams, stream); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_CONFIGURED); +} + +static gboolean avdtp_setconf_cmd(struct avdtp *session, uint8_t transaction, + struct setconf_req *req, unsigned int size) +{ + struct conf_rej rej; + struct avdtp_local_sep *sep; + struct avdtp_stream *stream; + uint8_t err, category = 0x00; + struct audio_device *dev; + bdaddr_t src, dst; + GSList *l; + + if (size < sizeof(struct setconf_req)) { + error("Too short getcap request"); + return FALSE; + } + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + if (sep->stream) { + err = AVDTP_SEP_IN_USE; + goto failed; + } + + avdtp_get_peers(session, &src, &dst); + dev = manager_get_device(&src, &dst, FALSE); + if (!dev) { + error("Unable to get a audio device object"); + err = AVDTP_BAD_STATE; + goto failed; + } + + switch (sep->info.type) { + case AVDTP_SEP_TYPE_SOURCE: + if (!dev->sink) { + btd_device_add_uuid(dev->btd_dev, A2DP_SINK_UUID); + if (!dev->sink) { + error("Unable to get a audio sink object"); + err = AVDTP_BAD_STATE; + goto failed; + } + } + break; + case AVDTP_SEP_TYPE_SINK: + if (!dev->source) { + btd_device_add_uuid(dev->btd_dev, A2DP_SOURCE_UUID); + if (!dev->source) { + error("Unable to get a audio source object"); + err = AVDTP_BAD_STATE; + goto failed; + } + } + break; + } + + stream = g_new0(struct avdtp_stream, 1); + stream->session = session; + stream->lsep = sep; + stream->rseid = req->int_seid; + stream->caps = caps_to_list(req->caps, + size - sizeof(struct setconf_req), + &stream->codec, + &stream->delay_reporting); + + /* Verify that the Media Transport capability's length = 0. Reject otherwise */ + for (l = stream->caps; l != NULL; l = g_slist_next(l)) { + struct avdtp_service_capability *cap = l->data; + + if (cap->category == AVDTP_MEDIA_TRANSPORT && cap->length != 0) { + err = AVDTP_BAD_MEDIA_TRANSPORT_FORMAT; + goto failed_stream; + } + } + + if (stream->delay_reporting && session->version < 0x0103) + session->version = 0x0103; + + if (sep->ind && sep->ind->set_configuration) { + if (!sep->ind->set_configuration(session, sep, stream, + stream->caps, + setconf_cb, + sep->user_data)) { + err = AVDTP_UNSUPPORTED_CONFIGURATION; + category = 0x00; + goto failed_stream; + } + } else { + if (!avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_SET_CONFIGURATION, NULL, 0)) { + stream_free(stream); + return FALSE; + } + + sep->stream = stream; + sep->info.inuse = 1; + session->streams = g_slist_append(session->streams, stream); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_CONFIGURED); + } + + return TRUE; + +failed_stream: + stream_free(stream); +failed: + rej.error = err; + rej.category = category; + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_SET_CONFIGURATION, &rej, sizeof(rej)); +} + +static gboolean avdtp_getconf_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, int size) +{ + GSList *l; + struct avdtp_local_sep *sep = NULL; + int rsp_size; + uint8_t err; + uint8_t buf[1024]; + uint8_t *ptr = buf; + + if (size < (int) sizeof(struct seid_req)) { + error("Too short getconf request"); + return FALSE; + } + + memset(buf, 0, sizeof(buf)); + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + if (!sep->stream || !sep->stream->caps) { + err = AVDTP_UNSUPPORTED_CONFIGURATION; + goto failed; + } + + for (l = sep->stream->caps, rsp_size = 0; l != NULL; l = g_slist_next(l)) { + struct avdtp_service_capability *cap = l->data; + + if (rsp_size + cap->length + 2 > (int) sizeof(buf)) + break; + + memcpy(ptr, cap, cap->length + 2); + rsp_size += cap->length + 2; + ptr += cap->length + 2; + } + + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_GET_CONFIGURATION, buf, rsp_size); + +failed: + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_GET_CONFIGURATION, &err, sizeof(err)); +} + +static gboolean avdtp_reconf_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, int size) +{ + struct conf_rej rej; + + rej.error = AVDTP_NOT_SUPPORTED_COMMAND; + rej.category = 0x00; + + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_RECONFIGURE, &rej, sizeof(rej)); +} + +static void check_seid_collision(struct pending_req *req, uint8_t id) +{ + struct seid_req *seid = req->data; + + if (seid->acp_seid == id) + req->collided = TRUE; +} + +static void check_start_collision(struct pending_req *req, uint8_t id) +{ + struct start_req *start = req->data; + struct seid *seid = &start->first_seid; + int count = 1 + req->data_size - sizeof(struct start_req); + int i; + + for (i = 0; i < count; i++, seid++) { + if (seid->seid == id) { + req->collided = TRUE; + return; + } + } +} + +static void check_suspend_collision(struct pending_req *req, uint8_t id) +{ + struct suspend_req *suspend = req->data; + struct seid *seid = &suspend->first_seid; + int count = 1 + req->data_size - sizeof(struct suspend_req); + int i; + + for (i = 0; i < count; i++, seid++) { + if (seid->seid == id) { + req->collided = TRUE; + return; + } + } +} + +static void avdtp_check_collision(struct avdtp *session, uint8_t cmd, + struct avdtp_stream *stream) +{ + struct pending_req *req = session->req; + + if (req == NULL || (req->signal_id != cmd && cmd != AVDTP_ABORT)) + return; + + if (cmd == AVDTP_ABORT) + cmd = req->signal_id; + + switch (cmd) { + case AVDTP_OPEN: + case AVDTP_CLOSE: + check_seid_collision(req, stream->rseid); + break; + case AVDTP_START: + check_start_collision(req, stream->rseid); + break; + case AVDTP_SUSPEND: + check_suspend_collision(req, stream->rseid); + break; + } +} + +static gboolean avdtp_open_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, unsigned int size) +{ + struct avdtp_local_sep *sep; + struct avdtp_stream *stream; + uint8_t err; + + if (size < sizeof(struct seid_req)) { + error("Too short abort request"); + return FALSE; + } + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + if (sep->state != AVDTP_STATE_CONFIGURED) { + err = AVDTP_BAD_STATE; + goto failed; + } + + stream = sep->stream; + + if (sep->ind && sep->ind->open) { + if (!sep->ind->open(session, sep, stream, &err, + sep->user_data)) + goto failed; + } + + avdtp_check_collision(session, AVDTP_OPEN, stream); + + if (!avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_OPEN, NULL, 0)) + return FALSE; + + stream->open_acp = TRUE; + session->pending_open = stream; + stream->timer = g_timeout_add_seconds(REQ_TIMEOUT, + stream_open_timeout, + stream); + + return TRUE; + +failed: + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_OPEN, &err, sizeof(err)); +} + +static gboolean avdtp_start_cmd(struct avdtp *session, uint8_t transaction, + struct start_req *req, unsigned int size) +{ + struct avdtp_local_sep *sep; + struct avdtp_stream *stream; + struct stream_rej rej; + struct seid *seid; + uint8_t err, failed_seid; + int seid_count, i; + + if (size < sizeof(struct start_req)) { + error("Too short start request"); + return FALSE; + } + + seid_count = 1 + size - sizeof(struct start_req); + + seid = &req->first_seid; + + for (i = 0; i < seid_count; i++, seid++) { + failed_seid = seid->seid; + + sep = find_local_sep_by_seid(session->server, + req->first_seid.seid); + if (!sep || !sep->stream) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + stream = sep->stream; + + /* Also reject start cmd if state is not open */ + if (sep->state != AVDTP_STATE_OPEN) { + err = AVDTP_BAD_STATE; + goto failed; + } + stream->starting = TRUE; + + if (sep->ind && sep->ind->start) { + if (!sep->ind->start(session, sep, stream, &err, + sep->user_data)) + goto failed; + } + + avdtp_check_collision(session, AVDTP_START, stream); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_STREAMING); + } + + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_START, NULL, 0); + +failed: + DBG("Rejecting (%d)", err); + memset(&rej, 0, sizeof(rej)); + rej.acp_seid = failed_seid; + rej.error = err; + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_START, &rej, sizeof(rej)); +} + +static gboolean avdtp_close_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, unsigned int size) +{ + struct avdtp_local_sep *sep; + struct avdtp_stream *stream; + uint8_t err; + + if (size < sizeof(struct seid_req)) { + error("Too short close request"); + return FALSE; + } + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep || !sep->stream) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + if (sep->state != AVDTP_STATE_OPEN && + sep->state != AVDTP_STATE_STREAMING) { + err = AVDTP_BAD_STATE; + goto failed; + } + + stream = sep->stream; + + if (sep->ind && sep->ind->close) { + if (!sep->ind->close(session, sep, stream, &err, + sep->user_data)) + goto failed; + } + + avdtp_check_collision(session, AVDTP_CLOSE, stream); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_CLOSING); + + if (!avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_CLOSE, NULL, 0)) + return FALSE; + + stream->timer = g_timeout_add_seconds(REQ_TIMEOUT, + stream_close_timeout, + stream); + + return TRUE; + +failed: + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_CLOSE, &err, sizeof(err)); +} + +static gboolean avdtp_suspend_cmd(struct avdtp *session, uint8_t transaction, + struct suspend_req *req, unsigned int size) +{ + struct avdtp_local_sep *sep; + struct avdtp_stream *stream; + struct stream_rej rej; + struct seid *seid; + uint8_t err, failed_seid; + int seid_count, i; + + if (size < sizeof(struct suspend_req)) { + error("Too short suspend request"); + return FALSE; + } + + seid_count = 1 + size - sizeof(struct suspend_req); + + seid = &req->first_seid; + + for (i = 0; i < seid_count; i++, seid++) { + failed_seid = seid->seid; + + sep = find_local_sep_by_seid(session->server, + req->first_seid.seid); + if (!sep || !sep->stream) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + stream = sep->stream; + + if (sep->state != AVDTP_STATE_STREAMING) { + err = AVDTP_BAD_STATE; + goto failed; + } + + if (sep->ind && sep->ind->suspend) { + if (!sep->ind->suspend(session, sep, stream, &err, + sep->user_data)) + goto failed; + } + + avdtp_check_collision(session, AVDTP_SUSPEND, stream); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_OPEN); + } + + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_SUSPEND, NULL, 0); + +failed: + memset(&rej, 0, sizeof(rej)); + rej.acp_seid = failed_seid; + rej.error = err; + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_SUSPEND, &rej, sizeof(rej)); +} + +static gboolean avdtp_abort_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, unsigned int size) +{ + struct avdtp_local_sep *sep; + uint8_t err; + gboolean ret; + + if (size < sizeof(struct seid_req)) { + error("Too short abort request"); + return FALSE; + } + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep || !sep->stream) + return TRUE; + + if (sep->ind && sep->ind->abort) + sep->ind->abort(session, sep, sep->stream, &err, + sep->user_data); + + avdtp_check_collision(session, AVDTP_ABORT, sep->stream); + + ret = avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_ABORT, NULL, 0); + if (ret) + avdtp_sep_set_state(session, sep, AVDTP_STATE_ABORTING); + + return ret; +} + +static gboolean avdtp_secctl_cmd(struct avdtp *session, uint8_t transaction, + struct seid_req *req, int size) +{ + return avdtp_unknown_cmd(session, transaction, AVDTP_SECURITY_CONTROL); +} + +static gboolean avdtp_delayreport_cmd(struct avdtp *session, + uint8_t transaction, + struct delay_req *req, + unsigned int size) +{ + struct avdtp_local_sep *sep; + struct avdtp_stream *stream; + uint8_t err; + + if (size < sizeof(struct delay_req)) { + error("Too short delay report request"); + return FALSE; + } + + sep = find_local_sep_by_seid(session->server, req->acp_seid); + if (!sep || !sep->stream) { + err = AVDTP_BAD_ACP_SEID; + goto failed; + } + + stream = sep->stream; + + if (sep->state != AVDTP_STATE_CONFIGURED && + sep->state != AVDTP_STATE_STREAMING) { + err = AVDTP_BAD_STATE; + goto failed; + } + + stream->delay = ntohs(req->delay); + + if (sep->ind && sep->ind->delayreport) { + if (!sep->ind->delayreport(session, sep, stream->rseid, + stream->delay, &err, + sep->user_data)) + goto failed; + } + + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_ACCEPT, + AVDTP_DELAY_REPORT, NULL, 0); + +failed: + return avdtp_send(session, transaction, AVDTP_MSG_TYPE_REJECT, + AVDTP_DELAY_REPORT, &err, sizeof(err)); +} + +static gboolean avdtp_parse_cmd(struct avdtp *session, uint8_t transaction, + uint8_t signal_id, void *buf, int size) +{ + switch (signal_id) { + case AVDTP_DISCOVER: + DBG("Received DISCOVER_CMD"); + return avdtp_discover_cmd(session, transaction, buf, size); + case AVDTP_GET_CAPABILITIES: + DBG("Received GET_CAPABILITIES_CMD"); + return avdtp_getcap_cmd(session, transaction, buf, size, + FALSE); + case AVDTP_GET_ALL_CAPABILITIES: + DBG("Received GET_ALL_CAPABILITIES_CMD"); + return avdtp_getcap_cmd(session, transaction, buf, size, TRUE); + case AVDTP_SET_CONFIGURATION: + DBG("Received SET_CONFIGURATION_CMD"); + return avdtp_setconf_cmd(session, transaction, buf, size); + case AVDTP_GET_CONFIGURATION: + DBG("Received GET_CONFIGURATION_CMD"); + return avdtp_getconf_cmd(session, transaction, buf, size); + case AVDTP_RECONFIGURE: + DBG("Received RECONFIGURE_CMD"); + return avdtp_reconf_cmd(session, transaction, buf, size); + case AVDTP_OPEN: + DBG("Received OPEN_CMD"); + return avdtp_open_cmd(session, transaction, buf, size); + case AVDTP_START: + DBG("Received START_CMD"); + return avdtp_start_cmd(session, transaction, buf, size); + case AVDTP_CLOSE: + DBG("Received CLOSE_CMD"); + return avdtp_close_cmd(session, transaction, buf, size); + case AVDTP_SUSPEND: + DBG("Received SUSPEND_CMD"); + return avdtp_suspend_cmd(session, transaction, buf, size); + case AVDTP_ABORT: + DBG("Received ABORT_CMD"); + return avdtp_abort_cmd(session, transaction, buf, size); + case AVDTP_SECURITY_CONTROL: + DBG("Received SECURITY_CONTROL_CMD"); + return avdtp_secctl_cmd(session, transaction, buf, size); + case AVDTP_DELAY_REPORT: + DBG("Received DELAY_REPORT_CMD"); + return avdtp_delayreport_cmd(session, transaction, buf, size); + default: + DBG("Received unknown request id %u", signal_id); + return avdtp_unknown_cmd(session, transaction, signal_id); + } +} + +enum avdtp_parse_result { PARSE_ERROR, PARSE_FRAGMENT, PARSE_SUCCESS }; + +static enum avdtp_parse_result avdtp_parse_data(struct avdtp *session, + void *buf, size_t size) +{ + struct avdtp_common_header *header = buf; + struct avdtp_single_header *single = (void *) session->buf; + struct avdtp_start_header *start = (void *) session->buf; + void *payload; + gsize payload_size; + + switch (header->packet_type) { + case AVDTP_PKT_TYPE_SINGLE: + if (size < sizeof(*single)) { + error("Received too small single packet (%zu bytes)", size); + return PARSE_ERROR; + } + if (session->in.active) { + error("SINGLE: Invalid AVDTP packet fragmentation"); + return PARSE_ERROR; + } + + payload = session->buf + sizeof(*single); + payload_size = size - sizeof(*single); + + session->in.active = TRUE; + session->in.data_size = 0; + session->in.no_of_packets = 1; + session->in.transaction = header->transaction; + session->in.message_type = header->message_type; + session->in.signal_id = single->signal_id; + + break; + case AVDTP_PKT_TYPE_START: + if (size < sizeof(*start)) { + error("Received too small start packet (%zu bytes)", size); + return PARSE_ERROR; + } + if (session->in.active) { + error("START: Invalid AVDTP packet fragmentation"); + return PARSE_ERROR; + } + + session->in.active = TRUE; + session->in.data_size = 0; + session->in.transaction = header->transaction; + session->in.message_type = header->message_type; + session->in.no_of_packets = start->no_of_packets; + session->in.signal_id = start->signal_id; + + payload = session->buf + sizeof(*start); + payload_size = size - sizeof(*start); + + break; + case AVDTP_PKT_TYPE_CONTINUE: + if (size < sizeof(struct avdtp_continue_header)) { + error("Received too small continue packet (%zu bytes)", + size); + return PARSE_ERROR; + } + if (!session->in.active) { + error("CONTINUE: Invalid AVDTP packet fragmentation"); + return PARSE_ERROR; + } + if (session->in.transaction != header->transaction) { + error("Continue transaction id doesn't match"); + return PARSE_ERROR; + } + if (session->in.no_of_packets <= 1) { + error("Too few continue packets"); + return PARSE_ERROR; + } + + payload = session->buf + sizeof(struct avdtp_continue_header); + payload_size = size - sizeof(struct avdtp_continue_header); + + break; + case AVDTP_PKT_TYPE_END: + if (size < sizeof(struct avdtp_continue_header)) { + error("Received too small end packet (%zu bytes)", size); + return PARSE_ERROR; + } + if (!session->in.active) { + error("END: Invalid AVDTP packet fragmentation"); + return PARSE_ERROR; + } + if (session->in.transaction != header->transaction) { + error("End transaction id doesn't match"); + return PARSE_ERROR; + } + if (session->in.no_of_packets > 1) { + error("Got an end packet too early"); + return PARSE_ERROR; + } + + payload = session->buf + sizeof(struct avdtp_continue_header); + payload_size = size - sizeof(struct avdtp_continue_header); + + break; + default: + error("Invalid AVDTP packet type 0x%02X", header->packet_type); + return PARSE_ERROR; + } + + if (session->in.data_size + payload_size > + sizeof(session->in.buf)) { + error("Not enough incoming buffer space!"); + return PARSE_ERROR; + } + + memcpy(session->in.buf + session->in.data_size, payload, payload_size); + session->in.data_size += payload_size; + + if (session->in.no_of_packets > 1) { + session->in.no_of_packets--; + DBG("Received AVDTP fragment. %d to go", + session->in.no_of_packets); + return PARSE_FRAGMENT; + } + + session->in.active = FALSE; + + return PARSE_SUCCESS; +} + +static gboolean session_cb(GIOChannel *chan, GIOCondition cond, + gpointer data) +{ + struct avdtp *session = data; + struct avdtp_common_header *header; + ssize_t size; + int fd; + + DBG(""); + + if (cond & G_IO_NVAL) + return FALSE; + + header = (void *) session->buf; + + if (cond & (G_IO_HUP | G_IO_ERR)) + goto failed; + + fd = g_io_channel_unix_get_fd(chan); + size = read(fd, session->buf, session->imtu); + if (size < 0) { + error("IO Channel read error"); + goto failed; + } + + if ((size_t) size < sizeof(struct avdtp_common_header)) { + error("Received too small packet (%zu bytes)", size); + goto failed; + } + + switch (avdtp_parse_data(session, session->buf, size)) { + case PARSE_ERROR: + goto failed; + case PARSE_FRAGMENT: + return TRUE; + case PARSE_SUCCESS: + break; + } + + if (session->in.message_type == AVDTP_MSG_TYPE_COMMAND) { + if (!avdtp_parse_cmd(session, session->in.transaction, + session->in.signal_id, + session->in.buf, + session->in.data_size)) { + error("Unable to handle command. Disconnecting"); + goto failed; + } + + if (session->ref == 1 && !session->streams && !session->req) + set_disconnect_timer(session); + + if (session->streams && session->dc_timer) + remove_disconnect_timer(session); + + if (session->req && session->req->collided) { + DBG("Collision detected"); + goto next; + } + + return TRUE; + } + + if (session->req == NULL) { + error("No pending request, ignoring message"); + return TRUE; + } + + if (header->transaction != session->req->transaction) { + error("Transaction label doesn't match"); + return TRUE; + } + + if (session->in.signal_id != session->req->signal_id) { + error("Response signal doesn't match"); + return TRUE; + } + + g_source_remove(session->req->timeout); + session->req->timeout = 0; + + switch (header->message_type) { + case AVDTP_MSG_TYPE_ACCEPT: + if (!avdtp_parse_resp(session, session->req->stream, + session->in.transaction, + session->in.signal_id, + session->in.buf, + session->in.data_size)) { + error("Unable to parse accept response"); + goto failed; + } + break; + case AVDTP_MSG_TYPE_REJECT: + if (!avdtp_parse_rej(session, session->req->stream, + session->in.transaction, + session->in.signal_id, + session->in.buf, + session->in.data_size)) { + error("Unable to parse reject response"); + goto failed; + } + break; + case AVDTP_MSG_TYPE_GEN_REJECT: + error("Received a General Reject message"); + break; + default: + error("Unknown message type 0x%02X", header->message_type); + break; + } + +next: + pending_req_free(session->req); + session->req = NULL; + + process_queue(session); + + return TRUE; + +failed: + connection_lost(session, EIO); + + return FALSE; +} + +static struct avdtp *find_session(GSList *list, const bdaddr_t *dst) +{ + for (; list != NULL; list = g_slist_next(list)) { + struct avdtp *s = list->data; + + if (bacmp(dst, &s->dst)) + continue; + + return s; + } + + return NULL; +} + +static uint16_t get_version(struct avdtp *session) +{ + struct btd_adapter *adapter; + struct btd_device *device; + const sdp_record_t *rec; + sdp_list_t *protos; + sdp_data_t *proto_desc; + char addr[18]; + uint16_t ver = 0x0100; + + adapter = manager_find_adapter(&session->server->src); + if (!adapter) + return ver; + + ba2str(&session->dst, addr); + device = adapter_find_device(adapter, addr); + if (!device) + return ver; + + rec = btd_device_get_record(device, A2DP_SINK_UUID); + if (!rec) + rec = btd_device_get_record(device, A2DP_SOURCE_UUID); + + if (!rec) + return ver; + + if (sdp_get_access_protos(rec, &protos) < 0) + return ver; + + proto_desc = sdp_get_proto_desc(protos, AVDTP_UUID); + if (proto_desc && proto_desc->dtd == SDP_UINT16) + ver = proto_desc->val.uint16; + + sdp_list_foreach(protos, (sdp_list_func_t) sdp_list_free, NULL); + sdp_list_free(protos, NULL); + + return ver; +} + +static struct avdtp *avdtp_get_internal(const bdaddr_t *src, const bdaddr_t *dst) +{ + struct avdtp_server *server; + struct avdtp *session; + + assert(src != NULL); + assert(dst != NULL); + + server = find_server(servers, src); + if (server == NULL) + return NULL; + + session = find_session(server->sessions, dst); + if (session) { + if (session->pending_auth) + return NULL; + else + return session; + } + + session = g_new0(struct avdtp, 1); + + session->server = server; + bacpy(&session->dst, dst); + session->ref = 1; + /* We don't use avdtp_set_state() here since this isn't a state change + * but just setting of the initial state */ + session->state = AVDTP_SESSION_STATE_DISCONNECTED; + + session->version = get_version(session); + + server->sessions = g_slist_append(server->sessions, session); + + return session; +} + +struct avdtp *avdtp_get(bdaddr_t *src, bdaddr_t *dst) +{ + struct avdtp *session; + + session = avdtp_get_internal(src, dst); + + if (!session) + return NULL; + + return avdtp_ref(session); +} + +static void avdtp_connect_cb(GIOChannel *chan, GError *err, gpointer user_data) +{ + struct avdtp *session = user_data; + char address[18]; + int err_no = EIO; + + if (err) { + err_no = err->code; + error("%s", err->message); + goto failed; + } + + if (!session->io) + session->io = g_io_channel_ref(chan); + + bt_io_get(chan, BT_IO_L2CAP, &err, + BT_IO_OPT_OMTU, &session->omtu, + BT_IO_OPT_IMTU, &session->imtu, + BT_IO_OPT_INVALID); + if (err) { + err_no = err->code; + error("%s", err->message); + goto failed; + } + + ba2str(&session->dst, address); + DBG("AVDTP: connected %s channel to %s", + session->pending_open ? "transport" : "signaling", + address); + + if (session->state == AVDTP_SESSION_STATE_CONNECTING) { + DBG("AVDTP imtu=%u, omtu=%u", session->imtu, session->omtu); + + session->buf = g_malloc0(MAX(session->imtu, session->omtu)); + avdtp_set_state(session, AVDTP_SESSION_STATE_CONNECTED); + + if (session->io_id) + g_source_remove(session->io_id); + + /* This watch should be low priority since otherwise the + * connect callback might be dispatched before the session + * callback if the kernel wakes us up at the same time for + * them. This could happen if a headset is very quick in + * sending the Start command after connecting the stream + * transport channel. + */ + session->io_id = g_io_add_watch_full(chan, + G_PRIORITY_LOW, + G_IO_IN | G_IO_ERR | G_IO_HUP + | G_IO_NVAL, + (GIOFunc) session_cb, session, + NULL); + + if (session->stream_setup) + set_disconnect_timer(session); + } else if (session->pending_open) + handle_transport_connect(session, chan, session->imtu, + session->omtu); + else + goto failed; + + process_queue(session); + + return; + +failed: + if (session->pending_open) { + struct avdtp_stream *stream = session->pending_open; + + handle_transport_connect(session, NULL, 0, 0); + + if (avdtp_abort(session, stream) < 0) + avdtp_sep_set_state(session, stream->lsep, + AVDTP_STATE_IDLE); + } else + connection_lost(session, err_no); +} + +static void auth_cb(DBusError *derr, void *user_data) +{ + struct avdtp *session = user_data; + GError *err = NULL; + + if (derr && dbus_error_is_set(derr)) { + error("Access denied: %s", derr->message); + connection_lost(session, EACCES); + return; + } + + if (!bt_io_accept(session->io, avdtp_connect_cb, session, NULL, + &err)) { + error("bt_io_accept: %s", err->message); + connection_lost(session, EACCES); + g_error_free(err); + return; + } + + /* This is so that avdtp_connect_cb will know to do the right thing + * with respect to the disconnect timer */ + session->stream_setup = TRUE; +} + +static void avdtp_confirm_cb(GIOChannel *chan, gpointer data) +{ + struct avdtp *session; + struct audio_device *dev; + char address[18]; + bdaddr_t src, dst; + int perr; + GError *err = NULL; + + bt_io_get(chan, BT_IO_L2CAP, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_DEST_BDADDR, &dst, + BT_IO_OPT_DEST, address, + BT_IO_OPT_INVALID); + if (err) { + error("%s", err->message); + g_error_free(err); + goto drop; + } + + DBG("AVDTP: incoming connect from %s", address); + + session = avdtp_get_internal(&src, &dst); + if (!session) + goto drop; + + /* This state (ie, session is already *connecting*) happens when the + * device initiates a connect (really a config'd L2CAP channel) even + * though there is a connect we initiated in progress. In sink.c & + * source.c, this state is referred to as XCASE connect:connect. + * Abort the device's channel in favor of our own. + */ + if (session->state == AVDTP_SESSION_STATE_CONNECTING) { + DBG("connect already in progress (XCASE connect:connect)"); + goto drop; + } + + if (session->pending_open && session->pending_open->open_acp) { + if (!bt_io_accept(chan, avdtp_connect_cb, session, NULL, NULL)) + goto drop; + return; + } + + if (session->io) { + error("Refusing unexpected connect from %s", address); + goto drop; + } + + dev = manager_get_device(&src, &dst, FALSE); + if (!dev) { + dev = manager_get_device(&src, &dst, TRUE); + if (!dev) { + error("Unable to get audio device object for %s", + address); + goto drop; + } + btd_device_add_uuid(dev->btd_dev, ADVANCED_AUDIO_UUID); + } + + session->io = g_io_channel_ref(chan); + avdtp_set_state(session, AVDTP_SESSION_STATE_CONNECTING); + + session->io_id = g_io_add_watch(chan, G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) session_cb, session); + + perr = audio_device_request_authorization(dev, ADVANCED_AUDIO_UUID, + auth_cb, session); + if (perr < 0) { + avdtp_set_state(session, AVDTP_SESSION_STATE_DISCONNECTED); + avdtp_unref(session); + goto drop; + } + + dev->auto_connect = auto_connect; + + return; + +drop: + g_io_channel_shutdown(chan, TRUE, NULL); +} + +static GIOChannel *l2cap_connect(struct avdtp *session) +{ + GError *err = NULL; + GIOChannel *io; + + io = bt_io_connect(BT_IO_L2CAP, avdtp_connect_cb, session, + NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &session->server->src, + BT_IO_OPT_DEST_BDADDR, &session->dst, + BT_IO_OPT_PSM, AVDTP_PSM, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_INVALID); + if (!io) { + error("%s", err->message); + g_error_free(err); + return NULL; + } + + return io; +} + +static void queue_request(struct avdtp *session, struct pending_req *req, + gboolean priority) +{ + if (priority) + session->prio_queue = g_slist_append(session->prio_queue, req); + else + session->req_queue = g_slist_append(session->req_queue, req); +} + +static uint8_t req_get_seid(struct pending_req *req) +{ + if (req->signal_id == AVDTP_DISCOVER) + return 0; + + return ((struct seid_req *) (req->data))->acp_seid; +} + +static int cancel_request(struct avdtp *session, int err) +{ + struct pending_req *req; + struct seid_req sreq; + struct avdtp_local_sep *lsep; + struct avdtp_stream *stream; + uint8_t seid; + struct avdtp_error averr; + + req = session->req; + session->req = NULL; + + avdtp_error_init(&averr, AVDTP_ERRNO, err); + + seid = req_get_seid(req); + if (seid) + stream = find_stream_by_rseid(session, seid); + else + stream = NULL; + + if (stream) { + stream->abort_int = TRUE; + lsep = stream->lsep; + } else + lsep = NULL; + + switch (req->signal_id) { + case AVDTP_RECONFIGURE: + error("Reconfigure: %s (%d)", strerror(err), err); + if (lsep && lsep->cfm && lsep->cfm->reconfigure) + lsep->cfm->reconfigure(session, lsep, stream, &averr, + lsep->user_data); + break; + case AVDTP_OPEN: + error("Open: %s (%d)", strerror(err), err); + if (lsep && lsep->cfm && lsep->cfm->open) + lsep->cfm->open(session, lsep, stream, &averr, + lsep->user_data); + break; + case AVDTP_START: + error("Start: %s (%d)", strerror(err), err); + if (lsep && lsep->cfm && lsep->cfm->start) { + lsep->cfm->start(session, lsep, stream, &averr, + lsep->user_data); + if (stream) + stream->starting = FALSE; + } + break; + case AVDTP_SUSPEND: + error("Suspend: %s (%d)", strerror(err), err); + if (lsep && lsep->cfm && lsep->cfm->suspend) + lsep->cfm->suspend(session, lsep, stream, &averr, + lsep->user_data); + break; + case AVDTP_CLOSE: + error("Close: %s (%d)", strerror(err), err); + if (lsep && lsep->cfm && lsep->cfm->close) { + lsep->cfm->close(session, lsep, stream, &averr, + lsep->user_data); + if (stream) + stream->close_int = FALSE; + } + break; + case AVDTP_SET_CONFIGURATION: + error("SetConfiguration: %s (%d)", strerror(err), err); + if (lsep && lsep->cfm && lsep->cfm->set_configuration) + lsep->cfm->set_configuration(session, lsep, stream, + &averr, lsep->user_data); + goto failed; + case AVDTP_DISCOVER: + error("Discover: %s (%d)", strerror(err), err); + goto failed; + case AVDTP_GET_CAPABILITIES: + error("GetCapabilities: %s (%d)", strerror(err), err); + goto failed; + case AVDTP_ABORT: + error("Abort: %s (%d)", strerror(err), err); + goto failed; + } + + if (!stream) + goto failed; + + memset(&sreq, 0, sizeof(sreq)); + sreq.acp_seid = seid; + + err = send_request(session, TRUE, stream, AVDTP_ABORT, &sreq, + sizeof(sreq)); + if (err < 0) { + error("Unable to send abort request"); + goto failed; + } + + goto done; + +failed: + connection_lost(session, err); +done: + pending_req_free(req); + return err; +} + +static gboolean request_timeout(gpointer user_data) +{ + struct avdtp *session = user_data; + + cancel_request(session, ETIMEDOUT); + + return FALSE; +} + +static int send_req(struct avdtp *session, gboolean priority, + struct pending_req *req) +{ + static int transaction = 0; + int err; + + if (session->state == AVDTP_SESSION_STATE_DISCONNECTED) { + session->io = l2cap_connect(session); + if (!session->io) { + err = -EIO; + goto failed; + } + avdtp_set_state(session, AVDTP_SESSION_STATE_CONNECTING); + } + + if (session->state < AVDTP_SESSION_STATE_CONNECTED || + session->req != NULL) { + queue_request(session, req, priority); + return 0; + } + + req->transaction = transaction++; + transaction %= 16; + + /* FIXME: Should we retry to send if the buffer + was not totally sent or in case of EINTR? */ + if (!avdtp_send(session, req->transaction, AVDTP_MSG_TYPE_COMMAND, + req->signal_id, req->data, req->data_size)) { + err = -EIO; + goto failed; + } + + session->req = req; + + req->timeout = g_timeout_add_seconds(req->signal_id == AVDTP_ABORT ? + ABORT_TIMEOUT : REQ_TIMEOUT, + request_timeout, + session); + return 0; + +failed: + g_free(req->data); + g_free(req); + return err; +} + +static int send_request(struct avdtp *session, gboolean priority, + struct avdtp_stream *stream, uint8_t signal_id, + void *buffer, size_t size) +{ + struct pending_req *req; + + if (stream && stream->abort_int && signal_id != AVDTP_ABORT) { + DBG("Unable to send requests while aborting"); + return -EINVAL; + } + + req = g_new0(struct pending_req, 1); + req->signal_id = signal_id; + req->data = g_malloc(size); + memcpy(req->data, buffer, size); + req->data_size = size; + req->stream = stream; + + return send_req(session, priority, req); +} + +static gboolean avdtp_discover_resp(struct avdtp *session, + struct discover_resp *resp, int size) +{ + int sep_count, i; + uint8_t getcap_cmd; + int ret = 0; + gboolean getcap_pending = FALSE; + + if (session->version >= 0x0103 && session->server->version >= 0x0103) + getcap_cmd = AVDTP_GET_ALL_CAPABILITIES; + else + getcap_cmd = AVDTP_GET_CAPABILITIES; + + sep_count = size / sizeof(struct seid_info); + + for (i = 0; i < sep_count; i++) { + struct avdtp_remote_sep *sep; + struct avdtp_stream *stream; + struct seid_req req; + + DBG("seid %d type %d media %d in use %d", + resp->seps[i].seid, resp->seps[i].type, + resp->seps[i].media_type, resp->seps[i].inuse); + + stream = find_stream_by_rseid(session, resp->seps[i].seid); + + sep = find_remote_sep(session->seps, resp->seps[i].seid); + if (!sep) { + if (resp->seps[i].inuse && !stream) + continue; + sep = g_new0(struct avdtp_remote_sep, 1); + session->seps = g_slist_append(session->seps, sep); + } + + sep->stream = stream; + sep->seid = resp->seps[i].seid; + sep->type = resp->seps[i].type; + sep->media_type = resp->seps[i].media_type; + + memset(&req, 0, sizeof(req)); + req.acp_seid = sep->seid; + + ret = send_request(session, TRUE, NULL, getcap_cmd, + &req, sizeof(req)); + if (ret < 0) + break; + getcap_pending = TRUE; + } + + if (!getcap_pending) + finalize_discovery(session, -ret); + + return TRUE; +} + +static gboolean avdtp_get_capabilities_resp(struct avdtp *session, + struct getcap_resp *resp, + unsigned int size) +{ + struct avdtp_remote_sep *sep; + uint8_t seid; + + /* Check for minimum required packet size includes: + * 1. getcap resp header + * 2. media transport capability (2 bytes) + * 3. media codec capability type + length (2 bytes) + * 4. the actual media codec elements + * */ + if (size < (sizeof(struct getcap_resp) + 4 + + sizeof(struct avdtp_media_codec_capability))) { + error("Too short getcap resp packet"); + return FALSE; + } + + seid = ((struct seid_req *) session->req->data)->acp_seid; + + sep = find_remote_sep(session->seps, seid); + + DBG("seid %d type %d media %d", sep->seid, + sep->type, sep->media_type); + + if (sep->caps) { + g_slist_free_full(sep->caps, g_free); + sep->caps = NULL; + sep->codec = NULL; + sep->delay_reporting = FALSE; + } + + sep->caps = caps_to_list(resp->caps, size - sizeof(struct getcap_resp), + &sep->codec, &sep->delay_reporting); + + return TRUE; +} + +static gboolean avdtp_set_configuration_resp(struct avdtp *session, + struct avdtp_stream *stream, + struct avdtp_single_header *resp, + int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + if (sep->cfm && sep->cfm->set_configuration) + sep->cfm->set_configuration(session, sep, stream, NULL, + sep->user_data); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_CONFIGURED); + + return TRUE; +} + +static gboolean avdtp_reconfigure_resp(struct avdtp *session, + struct avdtp_stream *stream, + struct avdtp_single_header *resp, int size) +{ + return TRUE; +} + +static gboolean avdtp_open_resp(struct avdtp *session, struct avdtp_stream *stream, + struct seid_rej *resp, int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + stream->io = l2cap_connect(session); + if (!stream->io) { + avdtp_sep_set_state(session, sep, AVDTP_STATE_IDLE); + return FALSE; + } + + session->pending_open = stream; + + return TRUE; +} + +static gboolean avdtp_start_resp(struct avdtp *session, + struct avdtp_stream *stream, + struct seid_rej *resp, int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + if (sep->cfm && sep->cfm->start) + sep->cfm->start(session, sep, stream, NULL, sep->user_data); + + /* We might be in STREAMING already if both sides send START_CMD at the + * same time and the one in SNK role doesn't reject it as it should */ + if (sep->state != AVDTP_STATE_STREAMING) + avdtp_sep_set_state(session, sep, AVDTP_STATE_STREAMING); + + return TRUE; +} + +static gboolean avdtp_close_resp(struct avdtp *session, + struct avdtp_stream *stream, + struct seid_rej *resp, int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + avdtp_sep_set_state(session, sep, AVDTP_STATE_CLOSING); + + close_stream(stream); + + return TRUE; +} + +static gboolean avdtp_suspend_resp(struct avdtp *session, + struct avdtp_stream *stream, + void *data, int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + avdtp_sep_set_state(session, sep, AVDTP_STATE_OPEN); + + if (sep->cfm && sep->cfm->suspend) + sep->cfm->suspend(session, sep, stream, NULL, sep->user_data); + + return TRUE; +} + +static gboolean avdtp_abort_resp(struct avdtp *session, + struct avdtp_stream *stream, + struct seid_rej *resp, int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + avdtp_sep_set_state(session, sep, AVDTP_STATE_ABORTING); + + if (sep->cfm && sep->cfm->abort) + sep->cfm->abort(session, sep, stream, NULL, sep->user_data); + + avdtp_sep_set_state(session, sep, AVDTP_STATE_IDLE); + + return TRUE; +} + +static gboolean avdtp_delay_report_resp(struct avdtp *session, + struct avdtp_stream *stream, + void *data, int size) +{ + struct avdtp_local_sep *sep = stream->lsep; + + if (sep->cfm && sep->cfm->delay_report) + sep->cfm->delay_report(session, sep, stream, NULL, sep->user_data); + + return TRUE; +} + +static gboolean avdtp_parse_resp(struct avdtp *session, + struct avdtp_stream *stream, + uint8_t transaction, uint8_t signal_id, + void *buf, int size) +{ + struct pending_req *next; + const char *get_all = ""; + + if (session->prio_queue) + next = session->prio_queue->data; + else if (session->req_queue) + next = session->req_queue->data; + else + next = NULL; + + switch (signal_id) { + case AVDTP_DISCOVER: + DBG("DISCOVER request succeeded"); + return avdtp_discover_resp(session, buf, size); + case AVDTP_GET_ALL_CAPABILITIES: + get_all = "ALL_"; + case AVDTP_GET_CAPABILITIES: + DBG("GET_%sCAPABILITIES request succeeded", get_all); + if (!avdtp_get_capabilities_resp(session, buf, size)) + return FALSE; + if (!(next && (next->signal_id == AVDTP_GET_CAPABILITIES || + next->signal_id == AVDTP_GET_ALL_CAPABILITIES))) + finalize_discovery(session, 0); + return TRUE; + } + + /* The remaining commands require an existing stream so bail out + * here if the stream got unexpectedly disconnected */ + if (!stream) { + DBG("AVDTP: stream was closed while waiting for reply"); + return TRUE; + } + + switch (signal_id) { + case AVDTP_SET_CONFIGURATION: + DBG("SET_CONFIGURATION request succeeded"); + return avdtp_set_configuration_resp(session, stream, + buf, size); + case AVDTP_RECONFIGURE: + DBG("RECONFIGURE request succeeded"); + return avdtp_reconfigure_resp(session, stream, buf, size); + case AVDTP_OPEN: + DBG("OPEN request succeeded"); + return avdtp_open_resp(session, stream, buf, size); + case AVDTP_SUSPEND: + DBG("SUSPEND request succeeded"); + return avdtp_suspend_resp(session, stream, buf, size); + case AVDTP_START: + DBG("START request succeeded"); + return avdtp_start_resp(session, stream, buf, size); + case AVDTP_CLOSE: + DBG("CLOSE request succeeded"); + return avdtp_close_resp(session, stream, buf, size); + case AVDTP_ABORT: + DBG("ABORT request succeeded"); + return avdtp_abort_resp(session, stream, buf, size); + case AVDTP_DELAY_REPORT: + DBG("DELAY_REPORT request succeeded"); + return avdtp_delay_report_resp(session, stream, buf, size); + } + + error("Unknown signal id in accept response: %u", signal_id); + return TRUE; +} + +static gboolean seid_rej_to_err(struct seid_rej *rej, unsigned int size, + struct avdtp_error *err) +{ + if (size < sizeof(struct seid_rej)) { + error("Too small packet for seid_rej"); + return FALSE; + } + + avdtp_error_init(err, 0x00, rej->error); + + return TRUE; +} + +static gboolean conf_rej_to_err(struct conf_rej *rej, unsigned int size, + struct avdtp_error *err) +{ + if (size < sizeof(struct conf_rej)) { + error("Too small packet for conf_rej"); + return FALSE; + } + + avdtp_error_init(err, rej->category, rej->error); + + return TRUE; +} + +static gboolean stream_rej_to_err(struct stream_rej *rej, unsigned int size, + struct avdtp_error *err, + uint8_t *acp_seid) +{ + if (size < sizeof(struct stream_rej)) { + error("Too small packet for stream_rej"); + return FALSE; + } + + avdtp_error_init(err, 0x00, rej->error); + + if (acp_seid) + *acp_seid = rej->acp_seid; + + return TRUE; +} + +static gboolean avdtp_parse_rej(struct avdtp *session, + struct avdtp_stream *stream, + uint8_t transaction, uint8_t signal_id, + void *buf, int size) +{ + struct avdtp_error err; + uint8_t acp_seid; + struct avdtp_local_sep *sep = stream ? stream->lsep : NULL; + + switch (signal_id) { + case AVDTP_DISCOVER: + if (!seid_rej_to_err(buf, size, &err)) + return FALSE; + error("DISCOVER request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + return TRUE; + case AVDTP_GET_CAPABILITIES: + case AVDTP_GET_ALL_CAPABILITIES: + if (!seid_rej_to_err(buf, size, &err)) + return FALSE; + error("GET_CAPABILITIES request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + return TRUE; + case AVDTP_OPEN: + if (!seid_rej_to_err(buf, size, &err)) + return FALSE; + error("OPEN request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->open) + sep->cfm->open(session, sep, stream, &err, + sep->user_data); + return TRUE; + case AVDTP_SET_CONFIGURATION: + if (!conf_rej_to_err(buf, size, &err)) + return FALSE; + error("SET_CONFIGURATION request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->set_configuration) + sep->cfm->set_configuration(session, sep, stream, + &err, sep->user_data); + return TRUE; + case AVDTP_RECONFIGURE: + if (!conf_rej_to_err(buf, size, &err)) + return FALSE; + error("RECONFIGURE request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->reconfigure) + sep->cfm->reconfigure(session, sep, stream, &err, + sep->user_data); + return TRUE; + case AVDTP_START: + if (!stream_rej_to_err(buf, size, &err, &acp_seid)) + return FALSE; + error("START request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->start) { + sep->cfm->start(session, sep, stream, &err, + sep->user_data); + stream->starting = FALSE; + } + return TRUE; + case AVDTP_SUSPEND: + if (!stream_rej_to_err(buf, size, &err, &acp_seid)) + return FALSE; + error("SUSPEND request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->suspend) + sep->cfm->suspend(session, sep, stream, &err, + sep->user_data); + return TRUE; + case AVDTP_CLOSE: + if (!stream_rej_to_err(buf, size, &err, &acp_seid)) + return FALSE; + error("CLOSE request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->close) { + sep->cfm->close(session, sep, stream, &err, + sep->user_data); + stream->close_int = FALSE; + } + return TRUE; + case AVDTP_ABORT: + if (!stream_rej_to_err(buf, size, &err, &acp_seid)) + return FALSE; + error("ABORT request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->abort) + sep->cfm->abort(session, sep, stream, &err, + sep->user_data); + return FALSE; + case AVDTP_DELAY_REPORT: + if (!stream_rej_to_err(buf, size, &err, &acp_seid)) + return FALSE; + error("DELAY_REPORT request rejected: %s (%d)", + avdtp_strerror(&err), err.err.error_code); + if (sep && sep->cfm && sep->cfm->delay_report) + sep->cfm->delay_report(session, sep, stream, &err, + sep->user_data); + return TRUE; + default: + error("Unknown reject response signal id: %u", signal_id); + return TRUE; + } +} + +gboolean avdtp_is_connected(const bdaddr_t *src, const bdaddr_t *dst) +{ + struct avdtp_server *server; + struct avdtp *session; + + server = find_server(servers, src); + if (!server) + return FALSE; + + session = find_session(server->sessions, dst); + if (!session) + return FALSE; + + if (session->state != AVDTP_SESSION_STATE_DISCONNECTED) + return TRUE; + + return FALSE; +} + +struct avdtp_service_capability *avdtp_stream_get_codec( + struct avdtp_stream *stream) +{ + GSList *l; + + for (l = stream->caps; l; l = l->next) { + struct avdtp_service_capability *cap = l->data; + + if (cap->category == AVDTP_MEDIA_CODEC) + return cap; + } + + return NULL; +} + +gboolean avdtp_stream_has_capability(struct avdtp_stream *stream, + struct avdtp_service_capability *cap) +{ + GSList *l; + struct avdtp_service_capability *stream_cap; + + for (l = stream->caps; l; l = g_slist_next(l)) { + stream_cap = l->data; + + if (stream_cap->category != cap->category || + stream_cap->length != cap->length) + continue; + + if (memcmp(stream_cap->data, cap->data, cap->length) == 0) + return TRUE; + } + + return FALSE; +} + +gboolean avdtp_stream_has_capabilities(struct avdtp_stream *stream, + GSList *caps) +{ + for (; caps; caps = g_slist_next(caps)) { + struct avdtp_service_capability *cap = caps->data; + + if (!avdtp_stream_has_capability(stream, cap)) + return FALSE; + } + + return TRUE; +} + +struct avdtp_remote_sep *avdtp_stream_get_remote_sep( + struct avdtp_stream *stream) +{ + return avdtp_get_remote_sep(stream->session, stream->rseid); +} + +gboolean avdtp_stream_get_transport(struct avdtp_stream *stream, int *sock, + uint16_t *imtu, uint16_t *omtu, + GSList **caps) +{ + if (stream->io == NULL) + return FALSE; + + if (sock) + *sock = g_io_channel_unix_get_fd(stream->io); + + if (omtu) + *omtu = stream->omtu; + + if (imtu) + *imtu = stream->imtu; + + if (caps) + *caps = stream->caps; + + return TRUE; +} + +static int process_queue(struct avdtp *session) +{ + GSList **queue, *l; + struct pending_req *req; + + if (session->req) + return 0; + + if (session->prio_queue) + queue = &session->prio_queue; + else + queue = &session->req_queue; + + if (!*queue) + return 0; + + l = *queue; + req = l->data; + + *queue = g_slist_remove(*queue, req); + + return send_req(session, FALSE, req); +} + +struct avdtp_remote_sep *avdtp_get_remote_sep(struct avdtp *session, + uint8_t seid) +{ + GSList *l; + + for (l = session->seps; l; l = l->next) { + struct avdtp_remote_sep *sep = l->data; + + if (sep->seid == seid) + return sep; + } + + return NULL; +} + +uint8_t avdtp_get_seid(struct avdtp_remote_sep *sep) +{ + return sep->seid; +} + +uint8_t avdtp_get_type(struct avdtp_remote_sep *sep) +{ + return sep->type; +} + +struct avdtp_service_capability *avdtp_get_codec(struct avdtp_remote_sep *sep) +{ + return sep->codec; +} + +gboolean avdtp_get_delay_reporting(struct avdtp_remote_sep *sep) +{ + return sep->delay_reporting; +} + +struct avdtp_stream *avdtp_get_stream(struct avdtp_remote_sep *sep) +{ + return sep->stream; +} + +struct avdtp_service_capability *avdtp_service_cap_new(uint8_t category, + void *data, int length) +{ + struct avdtp_service_capability *cap; + + if (category < AVDTP_MEDIA_TRANSPORT || category > AVDTP_DELAY_REPORTING) + return NULL; + + cap = g_malloc(sizeof(struct avdtp_service_capability) + length); + cap->category = category; + cap->length = length; + memcpy(cap->data, data, length); + + return cap; +} + +static gboolean process_discover(gpointer data) +{ + struct avdtp *session = data; + + finalize_discovery(session, 0); + + return FALSE; +} + +int avdtp_discover(struct avdtp *session, avdtp_discover_cb_t cb, + void *user_data) +{ + int err; + + if (session->discov_cb) + return -EBUSY; + + if (session->seps) { + session->discov_cb = cb; + session->user_data = user_data; + g_idle_add(process_discover, session); + return 0; + } + + err = send_request(session, FALSE, NULL, AVDTP_DISCOVER, NULL, 0); + if (err == 0) { + session->discov_cb = cb; + session->user_data = user_data; + } + + return err; +} + +gboolean avdtp_stream_remove_cb(struct avdtp *session, + struct avdtp_stream *stream, + unsigned int id) +{ + GSList *l; + struct stream_callback *cb; + + if (!stream) + return FALSE; + + for (cb = NULL, l = stream->callbacks; l != NULL; l = l->next) { + struct stream_callback *tmp = l->data; + if (tmp && tmp->id == id) { + cb = tmp; + break; + } + } + + if (!cb) + return FALSE; + + stream->callbacks = g_slist_remove(stream->callbacks, cb); + g_free(cb); + + return TRUE; +} + +unsigned int avdtp_stream_add_cb(struct avdtp *session, + struct avdtp_stream *stream, + avdtp_stream_state_cb cb, void *data) +{ + struct stream_callback *stream_cb; + static unsigned int id = 0; + + stream_cb = g_new(struct stream_callback, 1); + stream_cb->cb = cb; + stream_cb->user_data = data; + stream_cb->id = ++id; + + stream->callbacks = g_slist_append(stream->callbacks, stream_cb); + + return stream_cb->id; +} + +int avdtp_get_configuration(struct avdtp *session, struct avdtp_stream *stream) +{ + struct seid_req req; + + if (session->state < AVDTP_SESSION_STATE_CONNECTED) + return -EINVAL; + + memset(&req, 0, sizeof(req)); + req.acp_seid = stream->rseid; + + return send_request(session, FALSE, stream, AVDTP_GET_CONFIGURATION, + &req, sizeof(req)); +} + +static void copy_capabilities(gpointer data, gpointer user_data) +{ + struct avdtp_service_capability *src_cap = data; + struct avdtp_service_capability *dst_cap; + GSList **l = user_data; + + dst_cap = avdtp_service_cap_new(src_cap->category, src_cap->data, + src_cap->length); + + *l = g_slist_append(*l, dst_cap); +} + +int avdtp_set_configuration(struct avdtp *session, + struct avdtp_remote_sep *rsep, + struct avdtp_local_sep *lsep, + GSList *caps, + struct avdtp_stream **stream) +{ + struct setconf_req *req; + struct avdtp_stream *new_stream; + unsigned char *ptr; + int err, caps_len; + struct avdtp_service_capability *cap; + GSList *l; + + if (session->state != AVDTP_SESSION_STATE_CONNECTED) + return -ENOTCONN; + + if (!(lsep && rsep)) + return -EINVAL; + + DBG("%p: int_seid=%u, acp_seid=%u", session, + lsep->info.seid, rsep->seid); + + new_stream = g_new0(struct avdtp_stream, 1); + new_stream->session = session; + new_stream->lsep = lsep; + new_stream->rseid = rsep->seid; + + if (rsep->delay_reporting && lsep->delay_reporting) { + struct avdtp_service_capability *delay_reporting; + + delay_reporting = avdtp_service_cap_new(AVDTP_DELAY_REPORTING, + NULL, 0); + caps = g_slist_append(caps, delay_reporting); + new_stream->delay_reporting = TRUE; + } + + g_slist_foreach(caps, copy_capabilities, &new_stream->caps); + + /* Calculate total size of request */ + for (l = caps, caps_len = 0; l != NULL; l = g_slist_next(l)) { + cap = l->data; + caps_len += cap->length + 2; + } + + req = g_malloc0(sizeof(struct setconf_req) + caps_len); + + req->int_seid = lsep->info.seid; + req->acp_seid = rsep->seid; + + /* Copy the capabilities into the request */ + for (l = caps, ptr = req->caps; l != NULL; l = g_slist_next(l)) { + cap = l->data; + memcpy(ptr, cap, cap->length + 2); + ptr += cap->length + 2; + } + + err = send_request(session, FALSE, new_stream, + AVDTP_SET_CONFIGURATION, req, + sizeof(struct setconf_req) + caps_len); + if (err < 0) + stream_free(new_stream); + else { + lsep->info.inuse = 1; + lsep->stream = new_stream; + rsep->stream = new_stream; + session->streams = g_slist_append(session->streams, new_stream); + if (stream) + *stream = new_stream; + } + + g_free(req); + + return err; +} + +int avdtp_reconfigure(struct avdtp *session, GSList *caps, + struct avdtp_stream *stream) +{ + struct reconf_req *req; + unsigned char *ptr; + int caps_len, err; + GSList *l; + struct avdtp_service_capability *cap; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state != AVDTP_STATE_OPEN) + return -EINVAL; + + /* Calculate total size of request */ + for (l = caps, caps_len = 0; l != NULL; l = g_slist_next(l)) { + cap = l->data; + caps_len += cap->length + 2; + } + + req = g_malloc0(sizeof(struct reconf_req) + caps_len); + + req->acp_seid = stream->rseid; + + /* Copy the capabilities into the request */ + for (l = caps, ptr = req->caps; l != NULL; l = g_slist_next(l)) { + cap = l->data; + memcpy(ptr, cap, cap->length + 2); + ptr += cap->length + 2; + } + + err = send_request(session, FALSE, stream, AVDTP_RECONFIGURE, req, + sizeof(*req) + caps_len); + g_free(req); + + return err; +} + +int avdtp_open(struct avdtp *session, struct avdtp_stream *stream) +{ + struct seid_req req; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state > AVDTP_STATE_CONFIGURED) + return -EINVAL; + + memset(&req, 0, sizeof(req)); + req.acp_seid = stream->rseid; + + return send_request(session, FALSE, stream, AVDTP_OPEN, + &req, sizeof(req)); +} + +static gboolean start_timeout(gpointer user_data) +{ + struct avdtp_stream *stream = user_data; + struct avdtp *session = stream->session; + + stream->open_acp = 0; + + if (avdtp_start(session, stream) < 0) + error("wait_timeout: avdtp_start failed"); + + stream->start_timer = 0; + + return FALSE; +} + +int avdtp_start(struct avdtp *session, struct avdtp_stream *stream) +{ + struct start_req req; + int ret; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state != AVDTP_STATE_OPEN) + return -EINVAL; + + /* Recommendation 12: + * If the RD has configured and opened a stream it is also responsible + * to start the streaming via GAVDP_START. + */ + if (stream->open_acp) { + /* If timer already active wait it */ + if (stream->start_timer) + return 0; + + stream->start_timer = g_timeout_add_seconds(START_TIMEOUT, + start_timeout, + stream); + return 0; + } + + if (stream->close_int == TRUE) { + error("avdtp_start: rejecting start since close is initiated"); + return -EINVAL; + } + + if (stream->starting == TRUE) { + DBG("stream already started"); + return -EINPROGRESS; + } + + memset(&req, 0, sizeof(req)); + req.first_seid.seid = stream->rseid; + + ret = send_request(session, FALSE, stream, AVDTP_START, + &req, sizeof(req)); + if (ret == 0) + stream->starting = TRUE; + + return ret; +} + +int avdtp_close(struct avdtp *session, struct avdtp_stream *stream, + gboolean immediate) +{ + struct seid_req req; + int ret; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state < AVDTP_STATE_OPEN) + return -EINVAL; + + if (stream->close_int == TRUE) { + error("avdtp_close: rejecting since close is already initiated"); + return -EINVAL; + } + + if (immediate && session->req && stream == session->req->stream) + return avdtp_abort(session, stream); + + memset(&req, 0, sizeof(req)); + req.acp_seid = stream->rseid; + + ret = send_request(session, FALSE, stream, AVDTP_CLOSE, + &req, sizeof(req)); + if (ret == 0) + stream->close_int = TRUE; + + return ret; +} + +int avdtp_suspend(struct avdtp *session, struct avdtp_stream *stream) +{ + struct seid_req req; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state <= AVDTP_STATE_OPEN || stream->close_int) + return -EINVAL; + + memset(&req, 0, sizeof(req)); + req.acp_seid = stream->rseid; + + return send_request(session, FALSE, stream, AVDTP_SUSPEND, + &req, sizeof(req)); +} + +int avdtp_abort(struct avdtp *session, struct avdtp_stream *stream) +{ + struct seid_req req; + int ret; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state == AVDTP_STATE_ABORTING) + return -EINVAL; + + if (session->req && stream == session->req->stream) + return cancel_request(session, ECANCELED); + + memset(&req, 0, sizeof(req)); + req.acp_seid = stream->rseid; + + ret = send_request(session, TRUE, stream, AVDTP_ABORT, + &req, sizeof(req)); + if (ret == 0) + stream->abort_int = TRUE; + + return ret; +} + +int avdtp_delay_report(struct avdtp *session, struct avdtp_stream *stream, + uint16_t delay) +{ + struct delay_req req; + + if (!g_slist_find(session->streams, stream)) + return -EINVAL; + + if (stream->lsep->state != AVDTP_STATE_CONFIGURED && + stream->lsep->state != AVDTP_STATE_STREAMING) + return -EINVAL; + + if (!stream->delay_reporting || session->version < 0x0103 || + session->server->version < 0x0103) + return -EINVAL; + + stream->delay = delay; + + memset(&req, 0, sizeof(req)); + req.acp_seid = stream->rseid; + req.delay = htons(delay); + + return send_request(session, TRUE, stream, AVDTP_DELAY_REPORT, + &req, sizeof(req)); +} + +struct avdtp_local_sep *avdtp_register_sep(const bdaddr_t *src, uint8_t type, + uint8_t media_type, + uint8_t codec_type, + gboolean delay_reporting, + struct avdtp_sep_ind *ind, + struct avdtp_sep_cfm *cfm, + void *user_data) +{ + struct avdtp_server *server; + struct avdtp_local_sep *sep; + + server = find_server(servers, src); + if (!server) + return NULL; + + if (g_slist_length(server->seps) > MAX_SEID) + return NULL; + + sep = g_new0(struct avdtp_local_sep, 1); + + sep->state = AVDTP_STATE_IDLE; + sep->info.seid = g_slist_length(server->seps) + 1; + sep->info.type = type; + sep->info.media_type = media_type; + sep->codec = codec_type; + sep->ind = ind; + sep->cfm = cfm; + sep->user_data = user_data; + sep->server = server; + sep->delay_reporting = TRUE; + + DBG("SEP %p registered: type:%d codec:%d seid:%d", sep, + sep->info.type, sep->codec, sep->info.seid); + server->seps = g_slist_append(server->seps, sep); + + return sep; +} + +int avdtp_unregister_sep(struct avdtp_local_sep *sep) +{ + struct avdtp_server *server; + + if (!sep) + return -EINVAL; + + server = sep->server; + server->seps = g_slist_remove(server->seps, sep); + + if (sep->stream) + release_stream(sep->stream, sep->stream->session); + + DBG("SEP %p unregistered: type:%d codec:%d seid:%d", sep, + sep->info.type, sep->codec, sep->info.seid); + + g_free(sep); + + return 0; +} + +static GIOChannel *avdtp_server_socket(const bdaddr_t *src, gboolean master) +{ + GError *err = NULL; + GIOChannel *io; + + io = bt_io_listen(BT_IO_L2CAP, NULL, avdtp_confirm_cb, + NULL, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, src, + BT_IO_OPT_PSM, AVDTP_PSM, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_MASTER, master, + BT_IO_OPT_INVALID); + if (!io) { + error("%s", err->message); + g_error_free(err); + } + + return io; +} + +const char *avdtp_strerror(struct avdtp_error *err) +{ + if (err->category == AVDTP_ERRNO) + return strerror(err->err.posix_errno); + + switch(err->err.error_code) { + case AVDTP_BAD_HEADER_FORMAT: + return "Bad Header Format"; + case AVDTP_BAD_LENGTH: + return "Bad Packet Length"; + case AVDTP_BAD_ACP_SEID: + return "Bad Acceptor SEID"; + case AVDTP_SEP_IN_USE: + return "Stream End Point in Use"; + case AVDTP_SEP_NOT_IN_USE: + return "Stream End Point Not in Use"; + case AVDTP_BAD_SERV_CATEGORY: + return "Bad Service Category"; + case AVDTP_BAD_PAYLOAD_FORMAT: + return "Bad Payload format"; + case AVDTP_NOT_SUPPORTED_COMMAND: + return "Command Not Supported"; + case AVDTP_INVALID_CAPABILITIES: + return "Invalid Capabilities"; + case AVDTP_BAD_RECOVERY_TYPE: + return "Bad Recovery Type"; + case AVDTP_BAD_MEDIA_TRANSPORT_FORMAT: + return "Bad Media Transport Format"; + case AVDTP_BAD_RECOVERY_FORMAT: + return "Bad Recovery Format"; + case AVDTP_BAD_ROHC_FORMAT: + return "Bad Header Compression Format"; + case AVDTP_BAD_CP_FORMAT: + return "Bad Content Protetion Format"; + case AVDTP_BAD_MULTIPLEXING_FORMAT: + return "Bad Multiplexing Format"; + case AVDTP_UNSUPPORTED_CONFIGURATION: + return "Configuration not supported"; + case AVDTP_BAD_STATE: + return "Bad State"; + default: + return "Unknow error"; + } +} + +avdtp_state_t avdtp_sep_get_state(struct avdtp_local_sep *sep) +{ + return sep->state; +} + +void avdtp_get_peers(struct avdtp *session, bdaddr_t *src, bdaddr_t *dst) +{ + if (src) + bacpy(src, &session->server->src); + if (dst) + bacpy(dst, &session->dst); +} + +int avdtp_init(const bdaddr_t *src, GKeyFile *config, uint16_t *version) +{ + GError *err = NULL; + gboolean tmp, master = TRUE; + struct avdtp_server *server; + uint16_t ver = 0x0102; + + if (!config) + goto proceed; + + tmp = g_key_file_get_boolean(config, "General", + "Master", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else + master = tmp; + + tmp = g_key_file_get_boolean(config, "General", "AutoConnect", + &err); + if (err) + g_clear_error(&err); + else + auto_connect = tmp; + + if (g_key_file_get_boolean(config, "A2DP", "DelayReporting", NULL)) + ver = 0x0103; + +proceed: + server = g_new0(struct avdtp_server, 1); + if (!server) + return -ENOMEM; + + server->version = ver; + + if (version) + *version = server->version; + + server->io = avdtp_server_socket(src, master); + if (!server->io) { + g_free(server); + return -1; + } + + bacpy(&server->src, src); + + servers = g_slist_append(servers, server); + + return 0; +} + +void avdtp_exit(const bdaddr_t *src) +{ + struct avdtp_server *server; + GSList *l; + + server = find_server(servers, src); + if (!server) + return; + + l = server->sessions; + while (l) { + struct avdtp *session = l->data; + + l = l->next; + /* value of l pointer should be updated before invoking + * connection_lost since it internally uses avdtp_unref + * which operates on server->session list as well + */ + connection_lost(session, -ECONNABORTED); + } + + servers = g_slist_remove(servers, server); + + g_io_channel_shutdown(server->io, TRUE, NULL); + g_io_channel_unref(server->io); + g_free(server); +} + +gboolean avdtp_has_stream(struct avdtp *session, struct avdtp_stream *stream) +{ + return g_slist_find(session->streams, stream) ? TRUE : FALSE; +} + +gboolean avdtp_stream_setup_active(struct avdtp *session) +{ + return session->stream_setup; +} + +void avdtp_set_device_disconnect(struct avdtp *session, gboolean dev_dc) +{ + session->device_disconnect = dev_dc; +} + +unsigned int avdtp_add_state_cb(avdtp_session_state_cb cb, void *user_data) +{ + struct avdtp_state_callback *state_cb; + static unsigned int id = 0; + + state_cb = g_new(struct avdtp_state_callback, 1); + state_cb->cb = cb; + state_cb->user_data = user_data; + state_cb->id = ++id; + + avdtp_callbacks = g_slist_append(avdtp_callbacks, state_cb); + + return state_cb->id; +} + +gboolean avdtp_remove_state_cb(unsigned int id) +{ + GSList *l; + + for (l = avdtp_callbacks; l != NULL; l = l->next) { + struct avdtp_state_callback *cb = l->data; + if (cb && cb->id == id) { + avdtp_callbacks = g_slist_remove(avdtp_callbacks, cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} diff --git a/audio/avdtp.h b/audio/avdtp.h new file mode 100644 index 0000000..e294ded --- /dev/null +++ b/audio/avdtp.h @@ -0,0 +1,315 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +typedef enum { + AVDTP_SESSION_STATE_DISCONNECTED, + AVDTP_SESSION_STATE_CONNECTING, + AVDTP_SESSION_STATE_CONNECTED +} avdtp_session_state_t; + +struct avdtp; +struct avdtp_stream; +struct avdtp_local_sep; +struct avdtp_remote_sep; +struct avdtp_error { + uint8_t category; + union { + uint8_t error_code; + int posix_errno; + } err; +}; + +/* SEP capability categories */ +#define AVDTP_MEDIA_TRANSPORT 0x01 +#define AVDTP_REPORTING 0x02 +#define AVDTP_RECOVERY 0x03 +#define AVDTP_CONTENT_PROTECTION 0x04 +#define AVDTP_HEADER_COMPRESSION 0x05 +#define AVDTP_MULTIPLEXING 0x06 +#define AVDTP_MEDIA_CODEC 0x07 +#define AVDTP_DELAY_REPORTING 0x08 +#define AVDTP_ERRNO 0xff + +/* AVDTP error definitions */ +#define AVDTP_BAD_HEADER_FORMAT 0x01 +#define AVDTP_BAD_LENGTH 0x11 +#define AVDTP_BAD_ACP_SEID 0x12 +#define AVDTP_SEP_IN_USE 0x13 +#define AVDTP_SEP_NOT_IN_USE 0x14 +#define AVDTP_BAD_SERV_CATEGORY 0x17 +#define AVDTP_BAD_PAYLOAD_FORMAT 0x18 +#define AVDTP_NOT_SUPPORTED_COMMAND 0x19 +#define AVDTP_INVALID_CAPABILITIES 0x1A +#define AVDTP_BAD_RECOVERY_TYPE 0x22 +#define AVDTP_BAD_MEDIA_TRANSPORT_FORMAT 0x23 +#define AVDTP_BAD_RECOVERY_FORMAT 0x25 +#define AVDTP_BAD_ROHC_FORMAT 0x26 +#define AVDTP_BAD_CP_FORMAT 0x27 +#define AVDTP_BAD_MULTIPLEXING_FORMAT 0x28 +#define AVDTP_UNSUPPORTED_CONFIGURATION 0x29 +#define AVDTP_BAD_STATE 0x31 + +/* SEP types definitions */ +#define AVDTP_SEP_TYPE_SOURCE 0x00 +#define AVDTP_SEP_TYPE_SINK 0x01 + +/* Media types definitions */ +#define AVDTP_MEDIA_TYPE_AUDIO 0x00 +#define AVDTP_MEDIA_TYPE_VIDEO 0x01 +#define AVDTP_MEDIA_TYPE_MULTIMEDIA 0x02 + +typedef enum { + AVDTP_STATE_IDLE, + AVDTP_STATE_CONFIGURED, + AVDTP_STATE_OPEN, + AVDTP_STATE_STREAMING, + AVDTP_STATE_CLOSING, + AVDTP_STATE_ABORTING, +} avdtp_state_t; + +struct avdtp_service_capability { + uint8_t category; + uint8_t length; + uint8_t data[0]; +} __attribute__ ((packed)); + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct avdtp_media_codec_capability { + uint8_t rfa0:4; + uint8_t media_type:4; + uint8_t media_codec_type; + uint8_t data[0]; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct avdtp_media_codec_capability { + uint8_t media_type:4; + uint8_t rfa0:4; + uint8_t media_codec_type; + uint8_t data[0]; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif + +typedef void (*avdtp_session_state_cb) (struct audio_device *dev, + struct avdtp *session, + avdtp_session_state_t old_state, + avdtp_session_state_t new_state, + void *user_data); + +typedef void (*avdtp_stream_state_cb) (struct avdtp_stream *stream, + avdtp_state_t old_state, + avdtp_state_t new_state, + struct avdtp_error *err, + void *user_data); + +typedef void (*avdtp_set_configuration_cb) (struct avdtp *session, + struct avdtp_stream *stream, + struct avdtp_error *err); + +/* Callbacks for when a reply is received to a command that we sent */ +struct avdtp_sep_cfm { + void (*set_configuration) (struct avdtp *session, + struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, + void *user_data); + void (*get_configuration) (struct avdtp *session, + struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, + void *user_data); + void (*open) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data); + void (*start) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, struct avdtp_error *err, + void *user_data); + void (*suspend) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data); + void (*close) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data); + void (*abort) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data); + void (*reconfigure) (struct avdtp *session, + struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data); + void (*delay_report) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data); +}; + +/* Callbacks for indicating when we received a new command. The return value + * indicates whether the command should be rejected or accepted */ +struct avdtp_sep_ind { + gboolean (*get_capability) (struct avdtp *session, + struct avdtp_local_sep *sep, + gboolean get_all, + GSList **caps, uint8_t *err, + void *user_data); + gboolean (*set_configuration) (struct avdtp *session, + struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, + GSList *caps, + avdtp_set_configuration_cb cb, + void *user_data); + gboolean (*get_configuration) (struct avdtp *session, + struct avdtp_local_sep *lsep, + uint8_t *err, void *user_data); + gboolean (*open) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data); + gboolean (*start) (struct avdtp *session, struct avdtp_local_sep *lsep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data); + gboolean (*suspend) (struct avdtp *session, + struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data); + gboolean (*close) (struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data); + void (*abort) (struct avdtp *session, struct avdtp_local_sep *sep, + struct avdtp_stream *stream, uint8_t *err, + void *user_data); + gboolean (*reconfigure) (struct avdtp *session, + struct avdtp_local_sep *lsep, + uint8_t *err, void *user_data); + gboolean (*delayreport) (struct avdtp *session, + struct avdtp_local_sep *lsep, + uint8_t rseid, uint16_t delay, + uint8_t *err, void *user_data); +}; + +typedef void (*avdtp_discover_cb_t) (struct avdtp *session, GSList *seps, + struct avdtp_error *err, void *user_data); + +struct avdtp *avdtp_get(bdaddr_t *src, bdaddr_t *dst); + +void avdtp_unref(struct avdtp *session); +struct avdtp *avdtp_ref(struct avdtp *session); + +gboolean avdtp_is_connected(const bdaddr_t *src, const bdaddr_t *dst); + +struct avdtp_service_capability *avdtp_service_cap_new(uint8_t category, + void *data, int size); + +struct avdtp_remote_sep *avdtp_get_remote_sep(struct avdtp *session, + uint8_t seid); + +uint8_t avdtp_get_seid(struct avdtp_remote_sep *sep); + +uint8_t avdtp_get_type(struct avdtp_remote_sep *sep); + +struct avdtp_service_capability *avdtp_get_codec(struct avdtp_remote_sep *sep); + +gboolean avdtp_get_delay_reporting(struct avdtp_remote_sep *sep); + +struct avdtp_stream *avdtp_get_stream(struct avdtp_remote_sep *sep); + +int avdtp_discover(struct avdtp *session, avdtp_discover_cb_t cb, + void *user_data); + +gboolean avdtp_has_stream(struct avdtp *session, struct avdtp_stream *stream); + +unsigned int avdtp_stream_add_cb(struct avdtp *session, + struct avdtp_stream *stream, + avdtp_stream_state_cb cb, void *data); +gboolean avdtp_stream_remove_cb(struct avdtp *session, + struct avdtp_stream *stream, + unsigned int id); + +gboolean avdtp_stream_get_transport(struct avdtp_stream *stream, int *sock, + uint16_t *imtu, uint16_t *omtu, + GSList **caps); +struct avdtp_service_capability *avdtp_stream_get_codec( + struct avdtp_stream *stream); +gboolean avdtp_stream_has_capability(struct avdtp_stream *stream, + struct avdtp_service_capability *cap); +gboolean avdtp_stream_has_capabilities(struct avdtp_stream *stream, + GSList *caps); +struct avdtp_remote_sep *avdtp_stream_get_remote_sep( + struct avdtp_stream *stream); + +unsigned int avdtp_add_state_cb(avdtp_session_state_cb cb, void *user_data); + +gboolean avdtp_remove_state_cb(unsigned int id); + +int avdtp_set_configuration(struct avdtp *session, + struct avdtp_remote_sep *rsep, + struct avdtp_local_sep *lsep, + GSList *caps, + struct avdtp_stream **stream); + +int avdtp_get_configuration(struct avdtp *session, + struct avdtp_stream *stream); + +int avdtp_open(struct avdtp *session, struct avdtp_stream *stream); +int avdtp_reconfigure(struct avdtp *session, GSList *caps, + struct avdtp_stream *stream); +int avdtp_start(struct avdtp *session, struct avdtp_stream *stream); +int avdtp_suspend(struct avdtp *session, struct avdtp_stream *stream); +int avdtp_close(struct avdtp *session, struct avdtp_stream *stream, + gboolean immediate); +int avdtp_abort(struct avdtp *session, struct avdtp_stream *stream); +int avdtp_delay_report(struct avdtp *session, struct avdtp_stream *stream, + uint16_t delay); + +struct avdtp_local_sep *avdtp_register_sep(const bdaddr_t *src, uint8_t type, + uint8_t media_type, + uint8_t codec_type, + gboolean delay_reporting, + struct avdtp_sep_ind *ind, + struct avdtp_sep_cfm *cfm, + void *user_data); + +/* Find a matching pair of local and remote SEP ID's */ +struct avdtp_remote_sep *avdtp_find_remote_sep(struct avdtp *session, + struct avdtp_local_sep *lsep); + +int avdtp_unregister_sep(struct avdtp_local_sep *sep); + +avdtp_state_t avdtp_sep_get_state(struct avdtp_local_sep *sep); + +void avdtp_error_init(struct avdtp_error *err, uint8_t type, int id); +const char *avdtp_strerror(struct avdtp_error *err); +uint8_t avdtp_error_category(struct avdtp_error *err); +int avdtp_error_error_code(struct avdtp_error *err); +int avdtp_error_posix_errno(struct avdtp_error *err); + +void avdtp_get_peers(struct avdtp *session, bdaddr_t *src, bdaddr_t *dst); + +gboolean avdtp_stream_setup_active(struct avdtp *session); +void avdtp_set_device_disconnect(struct avdtp *session, gboolean dev_dc); + +int avdtp_init(const bdaddr_t *src, GKeyFile *config, uint16_t *version); +void avdtp_exit(const bdaddr_t *src); diff --git a/audio/avrcp.c b/audio/avrcp.c new file mode 100644 index 0000000..0af69bf --- /dev/null +++ b/audio/avrcp.c @@ -0,0 +1,1471 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * Copyright (C) 2011 Texas Instruments, Inc. + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "../src/adapter.h" +#include "../src/device.h" + +#include "log.h" +#include "error.h" +#include "device.h" +#include "manager.h" +#include "avctp.h" +#include "avrcp.h" +#include "sdpd.h" +#include "dbus-common.h" + +/* Company IDs for vendor dependent commands */ +#define IEEEID_BTSIG 0x001958 + +/* Error codes for metadata transfer */ +#define E_INVALID_COMMAND 0x00 +#define E_INVALID_PARAM 0x01 +#define E_PARAM_NOT_FOUND 0x02 +#define E_INTERNAL 0x03 + +/* Packet types */ +#define AVRCP_PACKET_TYPE_SINGLE 0x00 +#define AVRCP_PACKET_TYPE_START 0x01 +#define AVRCP_PACKET_TYPE_CONTINUING 0x02 +#define AVRCP_PACKET_TYPE_END 0x03 + +/* PDU types for metadata transfer */ +#define AVRCP_GET_CAPABILITIES 0x10 +#define AVRCP_LIST_PLAYER_ATTRIBUTES 0X11 +#define AVRCP_LIST_PLAYER_VALUES 0x12 +#define AVRCP_GET_CURRENT_PLAYER_VALUE 0x13 +#define AVRCP_SET_PLAYER_VALUE 0x14 +#define AVRCP_GET_PLAYER_ATTRIBUTE_TEXT 0x15 +#define AVRCP_GET_PLAYER_VALUE_TEXT 0x16 +#define AVRCP_DISPLAYABLE_CHARSET 0x17 +#define AVRCP_CT_BATTERY_STATUS 0x18 +#define AVRCP_GET_ELEMENT_ATTRIBUTES 0x20 +#define AVRCP_GET_PLAY_STATUS 0x30 +#define AVRCP_REGISTER_NOTIFICATION 0x31 +#define AVRCP_REQUEST_CONTINUING 0x40 +#define AVRCP_ABORT_CONTINUING 0x41 +#define AVRCP_SET_ABSOLUTE_VOLUME 0x50 + +/* Capabilities for AVRCP_GET_CAPABILITIES pdu */ +#define CAP_COMPANY_ID 0x02 +#define CAP_EVENTS_SUPPORTED 0x03 + +#define AVRCP_REGISTER_NOTIFICATION_PARAM_LENGTH 5 + +#define AVRCP_FEATURE_CATEGORY_1 0x0001 +#define AVRCP_FEATURE_CATEGORY_2 0x0002 +#define AVRCP_FEATURE_CATEGORY_3 0x0004 +#define AVRCP_FEATURE_CATEGORY_4 0x0008 +#define AVRCP_FEATURE_PLAYER_SETTINGS 0x0010 + +enum battery_status { + BATTERY_STATUS_NORMAL = 0, + BATTERY_STATUS_WARNING = 1, + BATTERY_STATUS_CRITICAL = 2, + BATTERY_STATUS_EXTERNAL = 3, + BATTERY_STATUS_FULL_CHARGE = 4, +}; + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct avrcp_header { + uint8_t company_id[3]; + uint8_t pdu_id; + uint8_t packet_type:2; + uint8_t rsvd:6; + uint16_t params_len; + uint8_t params[0]; +} __attribute__ ((packed)); +#define AVRCP_HEADER_LENGTH 7 + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct avrcp_header { + uint8_t company_id[3]; + uint8_t pdu_id; + uint8_t rsvd:6; + uint8_t packet_type:2; + uint16_t params_len; + uint8_t params[0]; +} __attribute__ ((packed)); +#define AVRCP_HEADER_LENGTH 7 + +#else +#error "Unknown byte order" +#endif + +#define AVRCP_MTU (AVC_MTU - AVC_HEADER_LENGTH) +#define AVRCP_PDU_MTU (AVRCP_MTU - AVRCP_HEADER_LENGTH) + +struct avrcp_server { + bdaddr_t src; + uint32_t tg_record_id; + uint32_t ct_record_id; + GSList *players; + struct avrcp_player *active_player; +}; + +struct pending_pdu { + uint8_t pdu_id; + GList *attr_ids; + uint16_t offset; +}; + +struct avrcp_player { + struct avrcp_server *server; + struct avctp *session; + struct audio_device *dev; + + unsigned int handler; + uint16_t registered_events; + uint8_t transaction_events[AVRCP_EVENT_LAST + 1]; + struct pending_pdu *pending_pdu; + + struct avrcp_player_cb *cb; + void *user_data; + GDestroyNotify destroy; +}; + +static GSList *servers = NULL; +static unsigned int avctp_id = 0; + +/* Company IDs supported by this device */ +static uint32_t company_ids[] = { + IEEEID_BTSIG, +}; + +static void register_volume_notification(struct avrcp_player *player); + +static sdp_record_t *avrcp_ct_record(void) +{ + sdp_list_t *svclass_id, *pfseq, *apseq, *root; + uuid_t root_uuid, l2cap, avctp, avrct; + sdp_profile_desc_t profile[1]; + sdp_list_t *aproto, *proto[2]; + sdp_record_t *record; + sdp_data_t *psm, *version, *features; + uint16_t lp = AVCTP_PSM; + uint16_t avrcp_ver = 0x0100, avctp_ver = 0x0103; + uint16_t feat = ( AVRCP_FEATURE_CATEGORY_1 | + AVRCP_FEATURE_CATEGORY_2 | + AVRCP_FEATURE_CATEGORY_3 | + AVRCP_FEATURE_CATEGORY_4 ); + + record = sdp_record_alloc(); + if (!record) + return NULL; + + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups(record, root); + + /* Service Class ID List */ + sdp_uuid16_create(&avrct, AV_REMOTE_SVCLASS_ID); + svclass_id = sdp_list_append(0, &avrct); + sdp_set_service_classes(record, svclass_id); + + /* Protocol Descriptor List */ + sdp_uuid16_create(&l2cap, L2CAP_UUID); + proto[0] = sdp_list_append(0, &l2cap); + psm = sdp_data_alloc(SDP_UINT16, &lp); + proto[0] = sdp_list_append(proto[0], psm); + apseq = sdp_list_append(0, proto[0]); + + sdp_uuid16_create(&avctp, AVCTP_UUID); + proto[1] = sdp_list_append(0, &avctp); + version = sdp_data_alloc(SDP_UINT16, &avctp_ver); + proto[1] = sdp_list_append(proto[1], version); + apseq = sdp_list_append(apseq, proto[1]); + + aproto = sdp_list_append(0, apseq); + sdp_set_access_protos(record, aproto); + + /* Bluetooth Profile Descriptor List */ + sdp_uuid16_create(&profile[0].uuid, AV_REMOTE_PROFILE_ID); + profile[0].version = avrcp_ver; + pfseq = sdp_list_append(0, &profile[0]); + sdp_set_profile_descs(record, pfseq); + + features = sdp_data_alloc(SDP_UINT16, &feat); + sdp_attr_add(record, SDP_ATTR_SUPPORTED_FEATURES, features); + + sdp_set_info_attr(record, "AVRCP CT", 0, 0); + + free(psm); + free(version); + sdp_list_free(proto[0], 0); + sdp_list_free(proto[1], 0); + sdp_list_free(apseq, 0); + sdp_list_free(pfseq, 0); + sdp_list_free(aproto, 0); + sdp_list_free(root, 0); + sdp_list_free(svclass_id, 0); + + return record; +} + +static sdp_record_t *avrcp_tg_record(void) +{ + sdp_list_t *svclass_id, *pfseq, *apseq, *root; + uuid_t root_uuid, l2cap, avctp, avrtg; + sdp_profile_desc_t profile[1]; + sdp_list_t *aproto, *proto[2]; + sdp_record_t *record; + sdp_data_t *psm, *version, *features; + uint16_t lp = AVCTP_PSM; + uint16_t avrcp_ver = 0x0104, avctp_ver = 0x0103; + uint16_t feat = ( AVRCP_FEATURE_CATEGORY_1 | + AVRCP_FEATURE_CATEGORY_2 | + AVRCP_FEATURE_CATEGORY_3 | + AVRCP_FEATURE_CATEGORY_4 | + AVRCP_FEATURE_PLAYER_SETTINGS ); + + record = sdp_record_alloc(); + if (!record) + return NULL; + + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups(record, root); + + /* Service Class ID List */ + sdp_uuid16_create(&avrtg, AV_REMOTE_TARGET_SVCLASS_ID); + svclass_id = sdp_list_append(0, &avrtg); + sdp_set_service_classes(record, svclass_id); + + /* Protocol Descriptor List */ + sdp_uuid16_create(&l2cap, L2CAP_UUID); + proto[0] = sdp_list_append(0, &l2cap); + psm = sdp_data_alloc(SDP_UINT16, &lp); + proto[0] = sdp_list_append(proto[0], psm); + apseq = sdp_list_append(0, proto[0]); + + sdp_uuid16_create(&avctp, AVCTP_UUID); + proto[1] = sdp_list_append(0, &avctp); + version = sdp_data_alloc(SDP_UINT16, &avctp_ver); + proto[1] = sdp_list_append(proto[1], version); + apseq = sdp_list_append(apseq, proto[1]); + + aproto = sdp_list_append(0, apseq); + sdp_set_access_protos(record, aproto); + + /* Bluetooth Profile Descriptor List */ + sdp_uuid16_create(&profile[0].uuid, AV_REMOTE_PROFILE_ID); + profile[0].version = avrcp_ver; + pfseq = sdp_list_append(0, &profile[0]); + sdp_set_profile_descs(record, pfseq); + + features = sdp_data_alloc(SDP_UINT16, &feat); + sdp_attr_add(record, SDP_ATTR_SUPPORTED_FEATURES, features); + + sdp_set_info_attr(record, "AVRCP TG", 0, 0); + + free(psm); + free(version); + sdp_list_free(proto[0], 0); + sdp_list_free(proto[1], 0); + sdp_list_free(apseq, 0); + sdp_list_free(aproto, 0); + sdp_list_free(pfseq, 0); + sdp_list_free(root, 0); + sdp_list_free(svclass_id, 0); + + return record; +} + +static unsigned int attr_get_max_val(uint8_t attr) +{ + switch (attr) { + case AVRCP_ATTRIBUTE_EQUALIZER: + return AVRCP_EQUALIZER_ON; + case AVRCP_ATTRIBUTE_REPEAT_MODE: + return AVRCP_REPEAT_MODE_GROUP; + case AVRCP_ATTRIBUTE_SHUFFLE: + return AVRCP_SHUFFLE_GROUP; + case AVRCP_ATTRIBUTE_SCAN: + return AVRCP_SCAN_GROUP; + } + + return 0; +} + +static const char *battery_status_to_str(enum battery_status status) +{ + switch (status) { + case BATTERY_STATUS_NORMAL: + return "normal"; + case BATTERY_STATUS_WARNING: + return "warning"; + case BATTERY_STATUS_CRITICAL: + return "critical"; + case BATTERY_STATUS_EXTERNAL: + return "external"; + case BATTERY_STATUS_FULL_CHARGE: + return "fullcharge"; + } + + return NULL; +} + +/* + * get_company_id: + * + * Get three-byte Company_ID from incoming AVRCP message + */ +static uint32_t get_company_id(const uint8_t cid[3]) +{ + return cid[0] << 16 | cid[1] << 8 | cid[2]; +} + +/* + * set_company_id: + * + * Set three-byte Company_ID into outgoing AVRCP message + */ +static void set_company_id(uint8_t cid[3], const uint32_t cid_in) +{ + cid[0] = cid_in >> 16; + cid[1] = cid_in >> 8; + cid[2] = cid_in; +} + +int avrcp_player_event(struct avrcp_player *player, uint8_t id, void *data) +{ + uint8_t buf[AVRCP_HEADER_LENGTH + 9]; + struct avrcp_header *pdu = (void *) buf; + uint16_t size; + int err; + + if (player->session == NULL) + return -ENOTCONN; + + if (!(player->registered_events & (1 << id))) + return 0; + + memset(buf, 0, sizeof(buf)); + + set_company_id(pdu->company_id, IEEEID_BTSIG); + + pdu->pdu_id = AVRCP_REGISTER_NOTIFICATION; + pdu->params[0] = id; + + DBG("id=%u", id); + + switch (id) { + case AVRCP_EVENT_STATUS_CHANGED: + size = 2; + pdu->params[1] = *((uint8_t *)data); + + break; + case AVRCP_EVENT_TRACK_CHANGED: + size = 9; + memcpy(&pdu->params[1], data, sizeof(uint64_t)); + + break; + case AVRCP_EVENT_TRACK_REACHED_END: + case AVRCP_EVENT_TRACK_REACHED_START: + size = 1; + break; + default: + error("Unknown event %u", id); + return -EINVAL; + } + + pdu->params_len = htons(size); + + err = avctp_send_vendordep(player->session, player->transaction_events[id], + AVC_CTYPE_CHANGED, AVC_SUBUNIT_PANEL, + buf, size + AVRCP_HEADER_LENGTH); + if (err < 0) + return err; + + /* Unregister event as per AVRCP 1.3 spec, section 5.4.2 */ + player->registered_events ^= 1 << id; + + return 0; +} + +static uint16_t player_write_media_attribute(struct avrcp_player *player, + uint32_t id, uint8_t *buf, + uint16_t *pos, + uint16_t *offset) +{ + uint16_t len; + uint16_t attr_len; + char valstr[20]; + void *value; + + DBG("%u", id); + + value = player->cb->get_metadata(id, player->user_data); + if (value == NULL) { + *offset = 0; + return 0; + } + + switch (id) { + case AVRCP_MEDIA_ATTRIBUTE_TRACK: + case AVRCP_MEDIA_ATTRIBUTE_N_TRACKS: + case AVRCP_MEDIA_ATTRIBUTE_DURATION: + snprintf(valstr, 20, "%u", GPOINTER_TO_UINT(value)); + value = valstr; + break; + } + + attr_len = strlen(value); + value = ((char *) value) + *offset; + len = attr_len - *offset; + + if (len > AVRCP_PDU_MTU - *pos) { + len = AVRCP_PDU_MTU - *pos; + *offset += len; + } else { + *offset = 0; + } + + memcpy(&buf[*pos], value, len); + *pos += len; + + return attr_len; +} + +static GList *player_fill_media_attribute(struct avrcp_player *player, + GList *attr_ids, uint8_t *buf, + uint16_t *pos, uint16_t *offset) +{ + struct media_attribute_header { + uint32_t id; + uint16_t charset; + uint16_t len; + } *hdr = NULL; + GList *l; + + for (l = attr_ids; l != NULL; l = g_list_delete_link(l, l)) { + uint32_t attr = GPOINTER_TO_UINT(l->data); + uint16_t attr_len; + + if (*offset == 0) { + if (*pos + sizeof(*hdr) >= AVRCP_PDU_MTU) + break; + + hdr = (void *) &buf[*pos]; + hdr->id = htonl(attr); + hdr->charset = htons(0x6A); /* Always use UTF-8 */ + *pos += sizeof(*hdr); + } + + attr_len = player_write_media_attribute(player, attr, buf, + pos, offset); + + if (hdr != NULL) + hdr->len = htons(attr_len); + + if (*offset > 0) + break; + } + + return l; +} + +static struct pending_pdu *pending_pdu_new(uint8_t pdu_id, GList *attr_ids, + unsigned int offset) +{ + struct pending_pdu *pending = g_new(struct pending_pdu, 1); + + pending->pdu_id = pdu_id; + pending->attr_ids = attr_ids; + pending->offset = offset; + + return pending; +} + +static gboolean player_abort_pending_pdu(struct avrcp_player *player) +{ + if (player->pending_pdu == NULL) + return FALSE; + + g_list_free(player->pending_pdu->attr_ids); + g_free(player->pending_pdu); + player->pending_pdu = NULL; + + return TRUE; +} + +static int player_set_attribute(struct avrcp_player *player, + uint8_t attr, uint8_t val) +{ + DBG("Change attribute: %u %u", attr, val); + + return player->cb->set_setting(attr, val, player->user_data); +} + +static int player_get_attribute(struct avrcp_player *player, uint8_t attr) +{ + int value; + + DBG("attr %u", attr); + + value = player->cb->get_setting(attr, player->user_data); + if (value < 0) + DBG("attr %u not supported by player", attr); + + return value; +} + +static uint8_t avrcp_handle_get_capabilities(struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction) +{ + uint16_t len = ntohs(pdu->params_len); + unsigned int i; + + if (len != 1) + goto err; + + DBG("id=%u", pdu->params[0]); + + switch (pdu->params[0]) { + case CAP_COMPANY_ID: + for (i = 0; i < G_N_ELEMENTS(company_ids); i++) { + set_company_id(&pdu->params[2 + i * 3], + company_ids[i]); + } + + pdu->params_len = htons(2 + (3 * G_N_ELEMENTS(company_ids))); + pdu->params[1] = G_N_ELEMENTS(company_ids); + + return AVC_CTYPE_STABLE; + case CAP_EVENTS_SUPPORTED: + pdu->params[1] = 4; + pdu->params[2] = AVRCP_EVENT_STATUS_CHANGED; + pdu->params[3] = AVRCP_EVENT_TRACK_CHANGED; + pdu->params[4] = AVRCP_EVENT_TRACK_REACHED_START; + pdu->params[5] = AVRCP_EVENT_TRACK_REACHED_END; + + pdu->params_len = htons(2 + pdu->params[1]); + return AVC_CTYPE_STABLE; + } + +err: + pdu->params_len = htons(1); + pdu->params[0] = E_INVALID_PARAM; + + return AVC_CTYPE_REJECTED; +} + +static uint8_t avrcp_handle_list_player_attributes(struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction) +{ + uint16_t len = ntohs(pdu->params_len); + unsigned int i; + + if (len != 0) { + pdu->params_len = htons(1); + pdu->params[0] = E_INVALID_PARAM; + return AVC_CTYPE_REJECTED; + } + + if (!player) + goto done; + + for (i = 1; i <= AVRCP_ATTRIBUTE_SCAN; i++) { + if (player_get_attribute(player, i) < 0) + continue; + + len++; + pdu->params[len] = i; + } + +done: + pdu->params[0] = len; + pdu->params_len = htons(len + 1); + + return AVC_CTYPE_STABLE; +} + +static uint8_t avrcp_handle_list_player_values(struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction) +{ + uint16_t len = ntohs(pdu->params_len); + unsigned int i; + + if (len != 1 || !player) + goto err; + + if (player_get_attribute(player, pdu->params[0]) < 0) + goto err; + + len = attr_get_max_val(pdu->params[0]); + + for (i = 1; i <= len; i++) + pdu->params[i] = i; + + pdu->params[0] = len; + pdu->params_len = htons(len + 1); + + return AVC_CTYPE_STABLE; + +err: + pdu->params_len = htons(1); + pdu->params[0] = E_INVALID_PARAM; + return AVC_CTYPE_REJECTED; +} + +static uint8_t avrcp_handle_get_element_attributes(struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction) +{ + uint16_t len = ntohs(pdu->params_len); + uint64_t *identifier = (uint64_t *) &pdu->params[0]; + uint16_t pos; + uint8_t nattr; + GList *attr_ids; + uint16_t offset; + + if (len < 9 || *identifier != 0) + goto err; + + nattr = pdu->params[8]; + + if (len < nattr * sizeof(uint32_t) + 1) + goto err; + + if (!nattr) { + /* + * Return all available information, at least + * title must be returned if there's a track selected. + */ + attr_ids = player->cb->list_metadata(player->user_data); + len = g_list_length(attr_ids); + } else { + unsigned int i; + uint32_t *attr = (uint32_t *) &pdu->params[9]; + + for (i = 0, len = 0, attr_ids = NULL; i < nattr; i++, attr++) { + uint32_t id = ntohl(bt_get_unaligned(attr)); + + /* Don't add invalid attributes */ + if (id == AVRCP_MEDIA_ATTRIBUTE_ILLEGAL || + id > AVRCP_MEDIA_ATTRIBUTE_LAST) + continue; + + len++; + attr_ids = g_list_prepend(attr_ids, + GUINT_TO_POINTER(id)); + } + + attr_ids = g_list_reverse(attr_ids); + } + + if (!len) + goto err; + + player_abort_pending_pdu(player); + pos = 1; + offset = 0; + attr_ids = player_fill_media_attribute(player, attr_ids, pdu->params, + &pos, &offset); + + if (attr_ids != NULL) { + player->pending_pdu = pending_pdu_new(pdu->pdu_id, attr_ids, + offset); + pdu->packet_type = AVRCP_PACKET_TYPE_START; + } + + pdu->params[0] = len; + pdu->params_len = htons(pos); + + return AVC_CTYPE_STABLE; +err: + pdu->params_len = htons(1); + pdu->params[0] = E_INVALID_PARAM; + return AVC_CTYPE_REJECTED; +} + +static uint8_t avrcp_handle_get_current_player_value(struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction) +{ + uint16_t len = ntohs(pdu->params_len); + uint8_t *settings; + unsigned int i; + + if (player == NULL || len <= 1 || pdu->params[0] != len - 1) + goto err; + + /* + * Save a copy of requested settings because we can override them + * while responding + */ + settings = g_memdup(&pdu->params[1], pdu->params[0]); + len = 0; + + /* + * From sec. 5.7 of AVRCP 1.3 spec, we should igore non-existent IDs + * and send a response with the existent ones. Only if all IDs are + * non-existent we should send an error. + */ + for (i = 0; i < pdu->params[0]; i++) { + int val; + + if (settings[i] < AVRCP_ATTRIBUTE_EQUALIZER || + settings[i] > AVRCP_ATTRIBUTE_SCAN) { + DBG("Ignoring %u", settings[i]); + continue; + } + + val = player_get_attribute(player, settings[i]); + if (val < 0) + continue; + + pdu->params[++len] = settings[i]; + pdu->params[++len] = val; + } + + g_free(settings); + + if (len) { + pdu->params[0] = len / 2; + pdu->params_len = htons(len + 1); + + return AVC_CTYPE_STABLE; + } + + error("No valid attributes in request"); + +err: + pdu->params_len = htons(1); + pdu->params[0] = E_INVALID_PARAM; + + return AVC_CTYPE_REJECTED; +} + +static uint8_t avrcp_handle_set_player_value(struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction) +{ + uint16_t len = ntohs(pdu->params_len); + unsigned int i; + uint8_t *param; + + if (len < 3 || len > 2 * pdu->params[0] + 1U) + goto err; + + /* + * From sec. 5.7 of AVRCP 1.3 spec, we should igore non-existent IDs + * and set the existent ones. Sec. 5.2.4 is not clear however how to + * indicate that a certain ID was not accepted. If at least one + * attribute is valid, we respond with no parameters. Otherwise an + * E_INVALID_PARAM is sent. + */ + for (len = 0, i = 0, param = &pdu->params[1]; i < pdu->params[0]; + i++, param += 2) { + if (player_set_attribute(player, param[0], param[1]) < 0) + continue; + + len++; + } + + if (len) { + pdu->params_len = 0; + + return AVC_CTYPE_ACCEPTED; + } + +err: + pdu->params_len = htons(1); + pdu->params[0] = E_INVALID_PARAM; + return AVC_CTYPE_REJECTED; +} + +static uint8_t avrcp_handle_displayable_charset(struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction) +{ + uint16_t len = ntohs(pdu->params_len); + + if (len < 3) { + pdu->params_len = htons(1); + pdu->params[0] = E_INVALID_PARAM; + return AVC_CTYPE_REJECTED; + } + + /* + * We acknowledge the commands, but we always use UTF-8 for + * encoding since CT is obliged to support it. + */ + pdu->params_len = 0; + return AVC_CTYPE_STABLE; +} + +static uint8_t avrcp_handle_ct_battery_status(struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction) +{ + uint16_t len = ntohs(pdu->params_len); + const char *valstr; + + if (len != 1) + goto err; + + valstr = battery_status_to_str(pdu->params[0]); + if (valstr == NULL) + goto err; + + pdu->params_len = 0; + + return AVC_CTYPE_STABLE; + +err: + pdu->params_len = htons(1); + pdu->params[0] = E_INVALID_PARAM; + return AVC_CTYPE_REJECTED; +} + +static uint8_t avrcp_handle_get_play_status(struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction) +{ + uint16_t len = ntohs(pdu->params_len); + uint32_t position; + uint32_t duration; + void *pduration; + + if (len != 0) { + pdu->params_len = htons(1); + pdu->params[0] = E_INVALID_PARAM; + return AVC_CTYPE_REJECTED; + } + + position = player->cb->get_position(player->user_data); + pduration = player->cb->get_metadata(AVRCP_MEDIA_ATTRIBUTE_DURATION, + player->user_data); + if (pduration != NULL) + duration = htonl(GPOINTER_TO_UINT(pduration)); + else + duration = htonl(UINT32_MAX); + + position = htonl(position); + + memcpy(&pdu->params[0], &duration, 4); + memcpy(&pdu->params[4], &position, 4); + pdu->params[8] = player->cb->get_status(player->user_data);; + + pdu->params_len = htons(9); + + return AVC_CTYPE_STABLE; +} + +static uint8_t avrcp_handle_register_notification(struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction) +{ + uint16_t len = ntohs(pdu->params_len); + uint64_t uid; + + /* + * 1 byte for EventID, 4 bytes for Playback interval but the latest + * one is applicable only for EVENT_PLAYBACK_POS_CHANGED. See AVRCP + * 1.3 spec, section 5.4.2. + */ + if (len != 5) + goto err; + + switch (pdu->params[0]) { + case AVRCP_EVENT_STATUS_CHANGED: + len = 2; + pdu->params[1] = player->cb->get_status(player->user_data); + + break; + case AVRCP_EVENT_TRACK_CHANGED: + len = 9; + uid = player->cb->get_uid(player->user_data); + memcpy(&pdu->params[1], &uid, sizeof(uint64_t)); + + break; + case AVRCP_EVENT_TRACK_REACHED_END: + case AVRCP_EVENT_TRACK_REACHED_START: + len = 1; + break; + default: + /* All other events are not supported yet */ + goto err; + } + + /* Register event and save the transaction used */ + player->registered_events |= (1 << pdu->params[0]); + player->transaction_events[pdu->params[0]] = transaction; + + pdu->params_len = htons(len); + + return AVC_CTYPE_INTERIM; + +err: + pdu->params_len = htons(1); + pdu->params[0] = E_INVALID_PARAM; + return AVC_CTYPE_REJECTED; +} + +static uint8_t avrcp_handle_request_continuing(struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction) +{ + uint16_t len = ntohs(pdu->params_len); + struct pending_pdu *pending; + + if (len != 1 || player->pending_pdu == NULL) + goto err; + + pending = player->pending_pdu; + + if (pending->pdu_id != pdu->params[0]) + goto err; + + + len = 0; + pending->attr_ids = player_fill_media_attribute(player, + pending->attr_ids, + pdu->params, &len, + &pending->offset); + pdu->pdu_id = pending->pdu_id; + + if (pending->attr_ids == NULL) { + g_free(player->pending_pdu); + player->pending_pdu = NULL; + pdu->packet_type = AVRCP_PACKET_TYPE_END; + } else { + pdu->packet_type = AVRCP_PACKET_TYPE_CONTINUING; + } + + pdu->params_len = htons(len); +#ifdef __TIZEN_PATCH__ + return AVC_CTYPE_ACCEPTED; +#else + return AVC_CTYPE_STABLE; +#endif +err: + pdu->params_len = htons(1); + pdu->params[0] = E_INVALID_PARAM; + return AVC_CTYPE_REJECTED; +} + +static uint8_t avrcp_handle_abort_continuing(struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction) +{ + uint16_t len = ntohs(pdu->params_len); + struct pending_pdu *pending; + + if (len != 1 || player->pending_pdu == NULL) + goto err; + + pending = player->pending_pdu; + + if (pending->pdu_id != pdu->params[0]) + goto err; + + player_abort_pending_pdu(player); + pdu->params_len = 0; + + return AVC_CTYPE_ACCEPTED; + +err: + pdu->params_len = htons(1); + pdu->params[0] = E_INVALID_PARAM; + return AVC_CTYPE_REJECTED; +} + +static struct pdu_handler { + uint8_t pdu_id; + uint8_t code; + uint8_t (*func) (struct avrcp_player *player, + struct avrcp_header *pdu, + uint8_t transaction); +} handlers[] = { + { AVRCP_GET_CAPABILITIES, AVC_CTYPE_STATUS, + avrcp_handle_get_capabilities }, + { AVRCP_LIST_PLAYER_ATTRIBUTES, AVC_CTYPE_STATUS, + avrcp_handle_list_player_attributes }, + { AVRCP_LIST_PLAYER_VALUES, AVC_CTYPE_STATUS, + avrcp_handle_list_player_values }, + { AVRCP_GET_ELEMENT_ATTRIBUTES, AVC_CTYPE_STATUS, + avrcp_handle_get_element_attributes }, + { AVRCP_GET_CURRENT_PLAYER_VALUE, AVC_CTYPE_STATUS, + avrcp_handle_get_current_player_value }, + { AVRCP_SET_PLAYER_VALUE, AVC_CTYPE_CONTROL, + avrcp_handle_set_player_value }, + { AVRCP_GET_PLAYER_ATTRIBUTE_TEXT, AVC_CTYPE_STATUS, + NULL }, + { AVRCP_GET_PLAYER_VALUE_TEXT, AVC_CTYPE_STATUS, + NULL }, + { AVRCP_DISPLAYABLE_CHARSET, AVC_CTYPE_STATUS, + avrcp_handle_displayable_charset }, + { AVRCP_CT_BATTERY_STATUS, AVC_CTYPE_STATUS, + avrcp_handle_ct_battery_status }, + { AVRCP_GET_PLAY_STATUS, AVC_CTYPE_STATUS, + avrcp_handle_get_play_status }, + { AVRCP_REGISTER_NOTIFICATION, AVC_CTYPE_NOTIFY, + avrcp_handle_register_notification }, + { AVRCP_REQUEST_CONTINUING, AVC_CTYPE_CONTROL, + avrcp_handle_request_continuing }, + { AVRCP_ABORT_CONTINUING, AVC_CTYPE_CONTROL, + avrcp_handle_abort_continuing }, + { }, +}; + +/* handle vendordep pdu inside an avctp packet */ +static size_t handle_vendordep_pdu(struct avctp *session, uint8_t transaction, + uint8_t *code, uint8_t *subunit, + uint8_t *operands, size_t operand_count, + void *user_data) +{ + struct avrcp_player *player = user_data; + struct pdu_handler *handler; + struct avrcp_header *pdu = (void *) operands; + uint32_t company_id = get_company_id(pdu->company_id); + + if (company_id != IEEEID_BTSIG) { + *code = AVC_CTYPE_NOT_IMPLEMENTED; + return 0; + } + + DBG("AVRCP PDU 0x%02X, company 0x%06X len 0x%04X", + pdu->pdu_id, company_id, pdu->params_len); + + pdu->packet_type = 0; + pdu->rsvd = 0; + + if (operand_count < AVRCP_HEADER_LENGTH) { + pdu->params[0] = E_INVALID_COMMAND; + goto err_metadata; + } + + for (handler = handlers; handler; handler++) { + if (handler->pdu_id == pdu->pdu_id) + break; + } + + if (!handler || handler->code != *code) { + pdu->params[0] = E_INVALID_COMMAND; + goto err_metadata; + } + + if (!handler->func) { + pdu->params[0] = E_INVALID_PARAM; + goto err_metadata; + } + + *code = handler->func(player, pdu, transaction); + + if (*code != AVC_CTYPE_REJECTED && + pdu->pdu_id != AVRCP_GET_ELEMENT_ATTRIBUTES && + pdu->pdu_id != AVRCP_REQUEST_CONTINUING && + pdu->pdu_id != AVRCP_ABORT_CONTINUING) + player_abort_pending_pdu(player); + + return AVRCP_HEADER_LENGTH + ntohs(pdu->params_len); + +err_metadata: + pdu->params_len = htons(1); + *code = AVC_CTYPE_REJECTED; + + return AVRCP_HEADER_LENGTH + 1; +} + +size_t avrcp_handle_vendor_reject(uint8_t *code, uint8_t *operands) +{ + struct avrcp_header *pdu = (void *) operands; + uint32_t company_id = get_company_id(pdu->company_id); + + *code = AVC_CTYPE_REJECTED; + pdu->params_len = htons(1); + pdu->params[0] = E_INTERNAL; + + DBG("rejecting AVRCP PDU 0x%02X, company 0x%06X len 0x%04X", + pdu->pdu_id, company_id, pdu->params_len); + + return AVRCP_HEADER_LENGTH + 1; +} + +static struct avrcp_server *find_server(GSList *list, const bdaddr_t *src) +{ + for (; list; list = list->next) { + struct avrcp_server *server = list->data; + + if (bacmp(&server->src, src) == 0) + return server; + } + + return NULL; +} + +static gboolean avrcp_handle_volume_changed(struct avctp *session, + uint8_t code, uint8_t subunit, + uint8_t *operands, size_t operand_count, + void *user_data) +{ + struct avrcp_player *player = user_data; + struct avrcp_header *pdu = (void *) operands; + uint8_t volume; + + if (code != AVC_CTYPE_INTERIM && code != AVC_CTYPE_CHANGED) + return FALSE; + + volume = pdu->params[1] & 0x7F; + + player->cb->set_volume(volume, player->dev, player->user_data); + + if (code == AVC_CTYPE_CHANGED) { + register_volume_notification(player); + return FALSE; + } + + return TRUE; +} + +static void register_volume_notification(struct avrcp_player *player) +{ + uint8_t buf[AVRCP_HEADER_LENGTH + AVRCP_REGISTER_NOTIFICATION_PARAM_LENGTH]; + struct avrcp_header *pdu = (void *) buf; + uint8_t length; + + memset(buf, 0, sizeof(buf)); + + set_company_id(pdu->company_id, IEEEID_BTSIG); + pdu->pdu_id = AVRCP_REGISTER_NOTIFICATION; + pdu->packet_type = AVRCP_PACKET_TYPE_SINGLE; + pdu->params[0] = AVRCP_EVENT_VOLUME_CHANGED; + pdu->params_len = htons(AVRCP_REGISTER_NOTIFICATION_PARAM_LENGTH); + + length = AVRCP_HEADER_LENGTH + ntohs(pdu->params_len); + + avctp_send_vendordep_req(player->session, AVC_CTYPE_NOTIFY, + AVC_SUBUNIT_PANEL, buf, length, + avrcp_handle_volume_changed, player); +} + +static void state_changed(struct audio_device *dev, avctp_state_t old_state, + avctp_state_t new_state, void *user_data) +{ + struct avrcp_server *server; + struct avrcp_player *player; + const sdp_record_t *rec; + sdp_list_t *list; + sdp_profile_desc_t *desc; + + server = find_server(servers, &dev->src); + if (!server) + return; + + player = server->active_player; + if (!player) + return; + + switch (new_state) { + case AVCTP_STATE_DISCONNECTED: + player->session = NULL; + player->dev = NULL; + player->registered_events = 0; + + if (player->handler) { + avctp_unregister_pdu_handler(player->handler); + player->handler = 0; + } + + break; + case AVCTP_STATE_CONNECTING: + player->session = avctp_connect(&dev->src, &dev->dst); + player->dev = dev; + + if (!player->handler) + player->handler = avctp_register_pdu_handler( + AVC_OP_VENDORDEP, + handle_vendordep_pdu, + player); + break; + case AVCTP_STATE_CONNECTED: + rec = btd_device_get_record(dev->btd_dev, AVRCP_TARGET_UUID); + if (rec == NULL) + return; + + if (sdp_get_profile_descs(rec, &list) < 0) + return; + + desc = list->data; + + if (desc && desc->version >= 0x0104) + register_volume_notification(player); + + sdp_list_free(list, free); + default: + return; + } +} + +gboolean avrcp_connect(struct audio_device *dev) +{ + struct avctp *session; + + session = avctp_connect(&dev->src, &dev->dst); + if (session) + return FALSE; + + return TRUE; +} + +void avrcp_disconnect(struct audio_device *dev) +{ + struct avctp *session; + + session = avctp_get(&dev->src, &dev->dst); + if (!session) + return; + + avctp_disconnect(session); +} + +int avrcp_register(DBusConnection *conn, const bdaddr_t *src, GKeyFile *config) +{ + sdp_record_t *record; + gboolean tmp, master = TRUE; + GError *err = NULL; + struct avrcp_server *server; + + if (config) { + tmp = g_key_file_get_boolean(config, "General", + "Master", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_error_free(err); + } else + master = tmp; + } + + server = g_new0(struct avrcp_server, 1); + if (!server) + return -ENOMEM; + + record = avrcp_tg_record(); + if (!record) { + error("Unable to allocate new service record"); + g_free(server); + return -1; + } + + if (add_record_to_server(src, record) < 0) { + error("Unable to register AVRCP target service record"); + g_free(server); + sdp_record_free(record); + return -1; + } + server->tg_record_id = record->handle; + + record = avrcp_ct_record(); + if (!record) { + error("Unable to allocate new service record"); + g_free(server); + return -1; + } + + if (add_record_to_server(src, record) < 0) { + error("Unable to register AVRCP service record"); + sdp_record_free(record); + g_free(server); + return -1; + } + server->ct_record_id = record->handle; + + if (avctp_register(src, master) < 0) { + remove_record_from_server(server->ct_record_id); + remove_record_from_server(server->tg_record_id); + g_free(server); + return -1; + } + + bacpy(&server->src, src); + + servers = g_slist_append(servers, server); + + return 0; +} + +static void player_destroy(gpointer data) +{ + struct avrcp_player *player = data; + + if (player->destroy) + player->destroy(player->user_data); + + player_abort_pending_pdu(player); + + if (player->handler) + avctp_unregister_pdu_handler(player->handler); + + g_free(player); +} + +void avrcp_unregister(const bdaddr_t *src) +{ + struct avrcp_server *server; + + server = find_server(servers, src); + if (!server) + return; + + g_slist_free_full(server->players, player_destroy); + + servers = g_slist_remove(servers, server); + + remove_record_from_server(server->ct_record_id); + remove_record_from_server(server->tg_record_id); + + avctp_unregister(&server->src); + g_free(server); + + if (servers) + return; + + if (avctp_id) { + avctp_remove_state_cb(avctp_id); + avctp_id = 0; + } +} + +struct avrcp_player *avrcp_register_player(const bdaddr_t *src, + struct avrcp_player_cb *cb, + void *user_data, + GDestroyNotify destroy) +{ + struct avrcp_server *server; + struct avrcp_player *player; + + server = find_server(servers, src); + if (!server) + return NULL; + + player = g_new0(struct avrcp_player, 1); + player->server = server; + player->cb = cb; + player->user_data = user_data; + player->destroy = destroy; + + if (!server->players) + server->active_player = player; + + if (!avctp_id) + avctp_id = avctp_add_state_cb(state_changed, NULL); + + server->players = g_slist_append(server->players, player); + + return player; +} + +void avrcp_unregister_player(struct avrcp_player *player) +{ + struct avrcp_server *server = player->server; + + server->players = g_slist_remove(server->players, player); + + if (server->active_player == player) + server->active_player = g_slist_nth_data(server->players, 0); + + player_destroy(player); +} + +static gboolean avrcp_handle_set_volume(struct avctp *session, + uint8_t code, uint8_t subunit, + uint8_t *operands, size_t operand_count, + void *user_data) +{ + struct avrcp_player *player = user_data; + struct avrcp_header *pdu = (void *) operands; + uint8_t volume; + + if (code == AVC_CTYPE_REJECTED || code == AVC_CTYPE_NOT_IMPLEMENTED) + return FALSE; + + volume = pdu->params[0] & 0x7F; + + player->cb->set_volume(volume, player->dev, player->user_data); + + return FALSE; +} + +int avrcp_set_volume(struct audio_device *dev, uint8_t volume) +{ + struct avrcp_server *server; + struct avrcp_player *player; + uint8_t buf[AVRCP_HEADER_LENGTH + 1]; + struct avrcp_header *pdu = (void *) buf; + + server = find_server(servers, &dev->src); + if (server == NULL) + return -EINVAL; + + player = server->active_player; + if (player == NULL) + return -ENOTSUP; + + if (player->session == NULL) + return -ENOTCONN; + + memset(buf, 0, sizeof(buf)); + + set_company_id(pdu->company_id, IEEEID_BTSIG); + + pdu->pdu_id = AVRCP_SET_ABSOLUTE_VOLUME; + pdu->params[0] = volume; + pdu->params_len = htons(1); + + DBG("volume=%u", volume); + + return avctp_send_vendordep_req(player->session, AVC_CTYPE_CONTROL, + AVC_SUBUNIT_PANEL, buf, sizeof(buf), + avrcp_handle_set_volume, player); +} diff --git a/audio/avrcp.h b/audio/avrcp.h new file mode 100644 index 0000000..bf11a6c --- /dev/null +++ b/audio/avrcp.h @@ -0,0 +1,107 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +/* player attributes */ +#define AVRCP_ATTRIBUTE_ILEGAL 0x00 +#define AVRCP_ATTRIBUTE_EQUALIZER 0x01 +#define AVRCP_ATTRIBUTE_REPEAT_MODE 0x02 +#define AVRCP_ATTRIBUTE_SHUFFLE 0x03 +#define AVRCP_ATTRIBUTE_SCAN 0x04 + +/* equalizer values */ +#define AVRCP_EQUALIZER_OFF 0x01 +#define AVRCP_EQUALIZER_ON 0x02 + +/* repeat mode values */ +#define AVRCP_REPEAT_MODE_OFF 0x01 +#define AVRCP_REPEAT_MODE_SINGLE 0x02 +#define AVRCP_REPEAT_MODE_ALL 0x03 +#define AVRCP_REPEAT_MODE_GROUP 0x04 + +/* shuffle values */ +#define AVRCP_SHUFFLE_OFF 0x01 +#define AVRCP_SHUFFLE_ALL 0x02 +#define AVRCP_SHUFFLE_GROUP 0x03 + +/* scan values */ +#define AVRCP_SCAN_OFF 0x01 +#define AVRCP_SCAN_ALL 0x02 +#define AVRCP_SCAN_GROUP 0x03 + +/* media attributes */ +#define AVRCP_MEDIA_ATTRIBUTE_ILLEGAL 0x00 +#define AVRCP_MEDIA_ATTRIBUTE_TITLE 0x01 +#define AVRCP_MEDIA_ATTRIBUTE_ARTIST 0x02 +#define AVRCP_MEDIA_ATTRIBUTE_ALBUM 0x03 +#define AVRCP_MEDIA_ATTRIBUTE_TRACK 0x04 +#define AVRCP_MEDIA_ATTRIBUTE_N_TRACKS 0x05 +#define AVRCP_MEDIA_ATTRIBUTE_GENRE 0x06 +#define AVRCP_MEDIA_ATTRIBUTE_DURATION 0x07 +#define AVRCP_MEDIA_ATTRIBUTE_LAST AVRCP_MEDIA_ATTRIBUTE_DURATION + +/* play status */ +#define AVRCP_PLAY_STATUS_STOPPED 0x00 +#define AVRCP_PLAY_STATUS_PLAYING 0x01 +#define AVRCP_PLAY_STATUS_PAUSED 0x02 +#define AVRCP_PLAY_STATUS_FWD_SEEK 0x03 +#define AVRCP_PLAY_STATUS_REV_SEEK 0x04 +#define AVRCP_PLAY_STATUS_ERROR 0xFF + +/* Notification events */ +#define AVRCP_EVENT_STATUS_CHANGED 0x01 +#define AVRCP_EVENT_TRACK_CHANGED 0x02 +#define AVRCP_EVENT_TRACK_REACHED_END 0x03 +#define AVRCP_EVENT_TRACK_REACHED_START 0x04 +#define AVRCP_EVENT_VOLUME_CHANGED 0x0d +#define AVRCP_EVENT_LAST AVRCP_EVENT_VOLUME_CHANGED + +struct avrcp_player_cb { + int (*get_setting) (uint8_t attr, void *user_data); + int (*set_setting) (uint8_t attr, uint8_t value, void *user_data); + uint64_t (*get_uid) (void *user_data); + void *(*get_metadata) (uint32_t id, void *user_data); + GList *(*list_metadata) (void *user_data); + uint8_t (*get_status) (void *user_data); + uint32_t (*get_position) (void *user_data); + void (*set_volume) (uint8_t volume, struct audio_device *dev, + void *user_data); +}; + +int avrcp_register(DBusConnection *conn, const bdaddr_t *src, GKeyFile *config); +void avrcp_unregister(const bdaddr_t *src); + +gboolean avrcp_connect(struct audio_device *dev); +void avrcp_disconnect(struct audio_device *dev); +int avrcp_set_volume(struct audio_device *dev, uint8_t volume); + +struct avrcp_player *avrcp_register_player(const bdaddr_t *src, + struct avrcp_player_cb *cb, + void *user_data, + GDestroyNotify destroy); +void avrcp_unregister_player(struct avrcp_player *player); + +int avrcp_player_event(struct avrcp_player *player, uint8_t id, void *data); + + +size_t avrcp_handle_vendor_reject(uint8_t *code, uint8_t *operands); diff --git a/audio/bluetooth.conf b/audio/bluetooth.conf new file mode 100644 index 0000000..55b51e4 --- /dev/null +++ b/audio/bluetooth.conf @@ -0,0 +1,36 @@ +# Please note that this ALSA configuration file fragment needs be enabled in +# /etc/asound.conf or a similar configuration file with directives similar to +# the following: +# +#@hooks [ +# { +# func load +# files [ +# "/etc/alsa/bluetooth.conf" +# ] +# errors false +# } +#] + +pcm.rawbluetooth { + @args [ ADDRESS ] + @args.ADDRESS { + type string + } + type bluetooth + device $ADDRESS +} + +pcm.bluetooth { + @args [ ADDRESS ] + @args.ADDRESS { + type string + } + type plug + slave { + pcm { + type bluetooth + device $ADDRESS + } + } +} diff --git a/audio/control.c b/audio/control.c new file mode 100644 index 0000000..c5a6a58 --- /dev/null +++ b/audio/control.c @@ -0,0 +1,279 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * Copyright (C) 2011 Texas Instruments, Inc. + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "log.h" +#include "error.h" +#include "device.h" +#include "manager.h" +#include "avctp.h" +#include "control.h" +#include "sdpd.h" +#include "glib-helper.h" +#include "dbus-common.h" + +static unsigned int avctp_id = 0; + +struct control { + struct audio_device *dev; + struct avctp *session; + + gboolean target; +}; + +static void state_changed(struct audio_device *dev, avctp_state_t old_state, + avctp_state_t new_state, void *user_data) +{ + struct control *control = dev->control; + gboolean value; + + switch (new_state) { + case AVCTP_STATE_DISCONNECTED: + control->session = NULL; + + if (old_state != AVCTP_STATE_CONNECTED) + break; + + value = FALSE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_CONTROL_INTERFACE, + "Disconnected", DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_CONTROL_INTERFACE, "Connected", + DBUS_TYPE_BOOLEAN, &value); + + break; + case AVCTP_STATE_CONNECTING: + if (control->session) + break; + + control->session = avctp_get(&dev->src, &dev->dst); + + break; + case AVCTP_STATE_CONNECTED: + value = TRUE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_CONTROL_INTERFACE, "Connected", + DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_CONTROL_INTERFACE, "Connected", + DBUS_TYPE_BOOLEAN, &value); + break; + default: + return; + } +} + +static DBusMessage *control_is_connected(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct control *control = device->control; + DBusMessage *reply; + dbus_bool_t connected; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + connected = (control->session != NULL); + + dbus_message_append_args(reply, DBUS_TYPE_BOOLEAN, &connected, + DBUS_TYPE_INVALID); + + return reply; +} + +static DBusMessage *volume_up(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct control *control = device->control; + int err; + + if (!control->session) + return btd_error_not_connected(msg); + + if (!control->target) + return btd_error_not_supported(msg); + + err = avctp_send_passthrough(control->session, VOL_UP_OP); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *volume_down(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct control *control = device->control; + int err; + + if (!control->session) + return btd_error_not_connected(msg); + + if (!control->target) + return btd_error_not_supported(msg); + + err = avctp_send_passthrough(control->session, VOL_DOWN_OP); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *control_get_properties(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + gboolean value; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + /* Connected */ + value = (device->control->session != NULL); + dict_append_entry(&dict, "Connected", DBUS_TYPE_BOOLEAN, &value); + + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static const GDBusMethodTable control_methods[] = { + { GDBUS_ASYNC_METHOD("IsConnected", + NULL, GDBUS_ARGS({ "connected", "b" }), + control_is_connected) }, + { GDBUS_METHOD("GetProperties", + NULL, GDBUS_ARGS({ "properties", "a{sv}" }), + control_get_properties) }, + { GDBUS_METHOD("VolumeUp", NULL, NULL, volume_up) }, + { GDBUS_METHOD("VolumeDown", NULL, NULL, volume_down) }, + { } +}; + +static const GDBusSignalTable control_signals[] = { + { GDBUS_DEPRECATED_SIGNAL("Connected", NULL) }, + { GDBUS_DEPRECATED_SIGNAL("Disconnected", NULL) }, + { GDBUS_SIGNAL("PropertyChanged", + GDBUS_ARGS({ "name", "s" }, { "value", "v" })) }, + { } +}; + +static void path_unregister(void *data) +{ + struct audio_device *dev = data; + struct control *control = dev->control; + + DBG("Unregistered interface %s on path %s", + AUDIO_CONTROL_INTERFACE, dev->path); + + if (control->session) + avctp_disconnect(control->session); + + g_free(control); + dev->control = NULL; +} + +void control_unregister(struct audio_device *dev) +{ + g_dbus_unregister_interface(dev->conn, dev->path, + AUDIO_CONTROL_INTERFACE); +} + +void control_update(struct control *control, uint16_t uuid16) +{ + if (uuid16 == AV_REMOTE_TARGET_SVCLASS_ID) + control->target = TRUE; +} + +struct control *control_init(struct audio_device *dev, uint16_t uuid16) +{ + struct control *control; + + if (!g_dbus_register_interface(dev->conn, dev->path, + AUDIO_CONTROL_INTERFACE, + control_methods, control_signals, NULL, + dev, path_unregister)) + return NULL; + + DBG("Registered interface %s on path %s", + AUDIO_CONTROL_INTERFACE, dev->path); + + control = g_new0(struct control, 1); + control->dev = dev; + + control_update(control, uuid16); + + if (!avctp_id) + avctp_id = avctp_add_state_cb(state_changed, NULL); + + return control; +} + +gboolean control_is_active(struct audio_device *dev) +{ + struct control *control = dev->control; + + if (control && control->session) + return TRUE; + + return FALSE; +} diff --git a/audio/control.h b/audio/control.h new file mode 100644 index 0000000..2219e5f --- /dev/null +++ b/audio/control.h @@ -0,0 +1,30 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define AUDIO_CONTROL_INTERFACE "org.bluez.Control" + +struct control *control_init(struct audio_device *dev, uint16_t uuid16); +void control_update(struct control *control, uint16_t uuid16); +void control_unregister(struct audio_device *dev); +gboolean control_is_active(struct audio_device *dev); diff --git a/audio/ctl_bluetooth.c b/audio/ctl_bluetooth.c new file mode 100644 index 0000000..a16f476 --- /dev/null +++ b/audio/ctl_bluetooth.c @@ -0,0 +1,383 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include +#include + +#include + +#include "ipc.h" + +#ifdef ENABLE_DEBUG +#define DBG(fmt, arg...) printf("DEBUG: %s: " fmt "\n" , __FUNCTION__ , ## arg) +#else +#define DBG(fmt, arg...) +#endif + +#define BLUETOOTH_MINVOL 0 +#define BLUETOOTH_MAXVOL 15 + +struct bluetooth_data { + snd_ctl_ext_t ext; + int sock; +}; + +enum { + BLUETOOTH_PLAYBACK, + BLUETOOTH_CAPTURE, +}; + +static const char *vol_devices[2] = { + [BLUETOOTH_PLAYBACK] = "Playback volume", + [BLUETOOTH_CAPTURE] = "Capture volume", +}; + +static void bluetooth_exit(struct bluetooth_data *data) +{ + if (data == NULL) + return; + + if (data->sock >= 0) + bt_audio_service_close(data->sock); + + free(data); +} + +static void bluetooth_close(snd_ctl_ext_t *ext) +{ + struct bluetooth_data *data = ext->private_data; + + DBG("ext %p", ext); + + bluetooth_exit(data); +} + +static int bluetooth_elem_count(snd_ctl_ext_t *ext) +{ + DBG("ext %p", ext); + + return 2; +} + +static int bluetooth_elem_list(snd_ctl_ext_t *ext, + unsigned int offset, snd_ctl_elem_id_t *id) +{ + DBG("ext %p offset %d id %p", ext, offset, id); + + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + + if (offset > 1) + return -EINVAL; + + snd_ctl_elem_id_set_name(id, vol_devices[offset]); + + return 0; +} + +static snd_ctl_ext_key_t bluetooth_find_elem(snd_ctl_ext_t *ext, + const snd_ctl_elem_id_t *id) +{ + const char *name = snd_ctl_elem_id_get_name(id); + int i; + + DBG("ext %p id %p name '%s'", ext, id, name); + + for (i = 0; i < 2; i++) + if (strcmp(name, vol_devices[i]) == 0) + return i; + + return SND_CTL_EXT_KEY_NOT_FOUND; +} + +static int bluetooth_get_attribute(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, + int *type, unsigned int *acc, unsigned int *count) +{ + DBG("ext %p key %ld", ext, key); + + *type = SND_CTL_ELEM_TYPE_INTEGER; + *acc = SND_CTL_EXT_ACCESS_READWRITE; + *count = 1; + + return 0; +} + +static int bluetooth_get_integer_info(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, + long *imin, long *imax, long *istep) +{ + DBG("ext %p key %ld", ext, key); + + *istep = 1; + *imin = BLUETOOTH_MINVOL; + *imax = BLUETOOTH_MAXVOL; + + return 0; +} + +static int bluetooth_send_ctl(struct bluetooth_data *data, + uint8_t mode, uint8_t key, struct bt_control_rsp *rsp) +{ + int ret; + struct bt_control_req *req = (void *) rsp; + bt_audio_error_t *err = (void *) rsp; + const char *type, *name; + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + req->h.type = BT_REQUEST; + req->h.name = BT_CONTROL; + req->h.length = sizeof(*req); + + req->mode = mode; + req->key = key; + + ret = send(data->sock, req, BT_SUGGESTED_BUFFER_SIZE, MSG_NOSIGNAL); + if (ret <= 0) { + SYSERR("Unable to request new volume value to server"); + return -errno; + } + + ret = recv(data->sock, rsp, BT_SUGGESTED_BUFFER_SIZE, 0); + if (ret <= 0) { + SNDERR("Unable to receive new volume value from server"); + return -errno; + } + + if (rsp->h.type == BT_ERROR) { + SNDERR("BT_CONTROL failed : %s (%d)", + strerror(err->posix_errno), + err->posix_errno); + return -err->posix_errno; + } + + type = bt_audio_strtype(rsp->h.type); + if (!type) { + SNDERR("Bogus message type %d " + "received from audio service", + rsp->h.type); + return -EINVAL; + } + + name = bt_audio_strname(rsp->h.name); + if (!name) { + SNDERR("Bogus message name %d " + "received from audio service", + rsp->h.name); + return -EINVAL; + } + + if (rsp->h.name != BT_CONTROL) { + SNDERR("Unexpected message %s received", type); + return -EINVAL; + } + + return 0; +} + +static int bluetooth_read_integer(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, + long *value) +{ + struct bluetooth_data *data = ext->private_data; + int ret; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_control_rsp *rsp = (void *) buf; + + DBG("ext %p key %ld", ext, key); + + memset(buf, 0, sizeof(buf)); + *value = 0; + + ret = bluetooth_send_ctl(data, key, 0, rsp); + if (ret == 0) + *value = rsp->key; + + return ret; +} + +static int bluetooth_write_integer(snd_ctl_ext_t *ext, snd_ctl_ext_key_t key, + long *value) +{ + struct bluetooth_data *data = ext->private_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_control_rsp *rsp = (void *) buf; + long current; + int ret, keyvalue; + + DBG("ext %p key %ld", ext, key); + + ret = bluetooth_read_integer(ext, key, ¤t); + if (ret < 0) + return ret; + + if (*value == current) + return 0; + + while (*value != current) { + keyvalue = (*value > current) ? BT_CONTROL_KEY_VOL_UP : + BT_CONTROL_KEY_VOL_DOWN; + + ret = bluetooth_send_ctl(data, key, keyvalue, rsp); + if (ret < 0) + break; + + current = keyvalue; + } + + return ret; +} + +static int bluetooth_read_event(snd_ctl_ext_t *ext, snd_ctl_elem_id_t *id, + unsigned int *event_mask) +{ + struct bluetooth_data *data = ext->private_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_control_ind *ind = (void *) buf; + int err; + const char *type, *name; + + DBG("ext %p id %p", ext, id); + + memset(buf, 0, sizeof(buf)); + + err = recv(data->sock, ind, BT_SUGGESTED_BUFFER_SIZE, MSG_DONTWAIT); + if (err < 0) { + err = -errno; + SNDERR("Failed while receiving data: %s (%d)", strerror(-err), + -err); + return err; + } + + type = bt_audio_strtype(ind->h.type); + if (!type) { + SNDERR("Bogus message type %d " + "received from audio service", + ind->h.type); + return -EAGAIN; + } + + name = bt_audio_strname(ind->h.name); + if (!name) { + SNDERR("Bogus message name %d " + "received from audio service", + ind->h.name); + return -EAGAIN; + } + + if (ind->h.name != BT_CONTROL) { + SNDERR("Unexpected message %s received", name); + return -EAGAIN; + } + + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_name(id, ind->mode == BLUETOOTH_PLAYBACK ? + vol_devices[BLUETOOTH_PLAYBACK] : + vol_devices[BLUETOOTH_CAPTURE]); + *event_mask = SND_CTL_EVENT_MASK_VALUE; + + return 1; +} + +static snd_ctl_ext_callback_t bluetooth_callback = { + .close = bluetooth_close, + .elem_count = bluetooth_elem_count, + .elem_list = bluetooth_elem_list, + .find_elem = bluetooth_find_elem, + .get_attribute = bluetooth_get_attribute, + .get_integer_info = bluetooth_get_integer_info, + .read_integer = bluetooth_read_integer, + .write_integer = bluetooth_write_integer, + .read_event = bluetooth_read_event, +}; + +static int bluetooth_init(struct bluetooth_data *data) +{ + int sk; + + if (!data) + return -EINVAL; + + memset(data, 0, sizeof(struct bluetooth_data)); + + data->sock = -1; + + sk = bt_audio_service_open(); + if (sk < 0) + return -errno; + + data->sock = sk; + + return 0; +} + +SND_CTL_PLUGIN_DEFINE_FUNC(bluetooth); + +SND_CTL_PLUGIN_DEFINE_FUNC(bluetooth) +{ + struct bluetooth_data *data; + int err; + + DBG("Bluetooth Control plugin"); + + data = malloc(sizeof(struct bluetooth_data)); + if (!data) { + err = -ENOMEM; + goto error; + } + + err = bluetooth_init(data); + if (err < 0) + goto error; + + data->ext.version = SND_CTL_EXT_VERSION; + data->ext.card_idx = -1; + + strncpy(data->ext.id, "bluetooth", sizeof(data->ext.id) - 1); + strncpy(data->ext.driver, "Bluetooth-Audio", sizeof(data->ext.driver) - 1); + strncpy(data->ext.name, "Bluetooth Audio", sizeof(data->ext.name) - 1); + strncpy(data->ext.longname, "Bluetooth Audio", sizeof(data->ext.longname) - 1); + strncpy(data->ext.mixername, "Bluetooth Audio", sizeof(data->ext.mixername) - 1); + + data->ext.callback = &bluetooth_callback; + data->ext.poll_fd = data->sock; + data->ext.private_data = data; + + err = snd_ctl_ext_create(&data->ext, name, mode); + if (err < 0) + goto error; + + *handlep = data->ext.handle; + + return 0; + +error: + bluetooth_exit(data); + + return err; +} + +SND_CTL_PLUGIN_SYMBOL(bluetooth); diff --git a/audio/device.c b/audio/device.c new file mode 100644 index 0000000..b7b993e --- /dev/null +++ b/audio/device.c @@ -0,0 +1,872 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "log.h" +#include "../src/adapter.h" +#include "../src/device.h" + +#include "error.h" +#include "ipc.h" +#include "dbus-common.h" +#include "device.h" +#include "unix.h" +#include "avdtp.h" +#include "control.h" +#include "avctp.h" +#include "avrcp.h" +#include "headset.h" +#include "gateway.h" +#include "sink.h" +#include "source.h" + +#define AUDIO_INTERFACE "org.bluez.Audio" + +#define CONTROL_CONNECT_TIMEOUT 2 +#define AVDTP_CONNECT_TIMEOUT 1 +#define AVDTP_CONNECT_TIMEOUT_BOOST 1 +#define HEADSET_CONNECT_TIMEOUT 1 + +typedef enum { + AUDIO_STATE_DISCONNECTED, + AUDIO_STATE_CONNECTING, + AUDIO_STATE_CONNECTED, +} audio_state_t; + +struct service_auth { + service_auth_cb cb; + void *user_data; +}; + +struct dev_priv { + audio_state_t state; + + headset_state_t hs_state; + sink_state_t sink_state; + avctp_state_t avctp_state; + GSList *auths; + + DBusMessage *conn_req; + DBusMessage *dc_req; + + guint control_timer; + guint avdtp_timer; + guint headset_timer; + guint dc_id; + + gboolean disconnecting; + gboolean authorized; + guint auth_idle_id; +}; + +static unsigned int sink_callback_id = 0; +static unsigned int avctp_callback_id = 0; +static unsigned int avdtp_callback_id = 0; +static unsigned int headset_callback_id = 0; + +static void device_free(struct audio_device *dev) +{ + struct dev_priv *priv = dev->priv; + + if (dev->conn) + dbus_connection_unref(dev->conn); + + btd_device_unref(dev->btd_dev); + + if (priv) { + if (priv->auths) + audio_device_cancel_authorization(dev, NULL, NULL); + if (priv->control_timer) + g_source_remove(priv->control_timer); + if (priv->avdtp_timer) + g_source_remove(priv->avdtp_timer); + if (priv->headset_timer) + g_source_remove(priv->headset_timer); + if (priv->dc_req) + dbus_message_unref(priv->dc_req); + if (priv->conn_req) + dbus_message_unref(priv->conn_req); + if (priv->dc_id) + device_remove_disconnect_watch(dev->btd_dev, + priv->dc_id); + g_free(priv); + } + + g_free(dev->path); + g_free(dev); +} + +static const char *state2str(audio_state_t state) +{ + switch (state) { + case AUDIO_STATE_DISCONNECTED: + return "disconnected"; + case AUDIO_STATE_CONNECTING: + return "connecting"; + case AUDIO_STATE_CONNECTED: + return "connected"; + default: + error("Invalid audio state %d", state); + return NULL; + } +} + +static gboolean control_connect_timeout(gpointer user_data) +{ + struct audio_device *dev = user_data; + + dev->priv->control_timer = 0; + + if (dev->control) + avrcp_connect(dev); + + return FALSE; +} + +static gboolean device_set_control_timer(struct audio_device *dev) +{ + struct dev_priv *priv = dev->priv; + + if (!dev->control) + return FALSE; + + if (priv->control_timer) + return FALSE; + + priv->control_timer = g_timeout_add_seconds(CONTROL_CONNECT_TIMEOUT, + control_connect_timeout, + dev); + + return TRUE; +} + +static void device_remove_control_timer(struct audio_device *dev) +{ + if (dev->priv->control_timer) + g_source_remove(dev->priv->control_timer); + dev->priv->control_timer = 0; +} + +static void device_remove_avdtp_timer(struct audio_device *dev) +{ + if (dev->priv->avdtp_timer) + g_source_remove(dev->priv->avdtp_timer); + dev->priv->avdtp_timer = 0; +} + +static void device_remove_headset_timer(struct audio_device *dev) +{ + if (dev->priv->headset_timer) + g_source_remove(dev->priv->headset_timer); + dev->priv->headset_timer = 0; +} + +static void disconnect_cb(struct btd_device *btd_dev, gboolean removal, + void *user_data) +{ + struct audio_device *dev = user_data; + struct dev_priv *priv = dev->priv; + + if (priv->state == AUDIO_STATE_DISCONNECTED) + return; + + if (priv->disconnecting) + return; + + priv->disconnecting = TRUE; + + device_remove_control_timer(dev); + device_remove_avdtp_timer(dev); + device_remove_headset_timer(dev); + + if (dev->control) + avrcp_disconnect(dev); + + if (dev->sink && priv->sink_state != SINK_STATE_DISCONNECTED) + sink_shutdown(dev->sink); + else if (priv->hs_state != HEADSET_STATE_DISCONNECTED) + headset_shutdown(dev); + else + priv->disconnecting = FALSE; +} + +static void device_set_state(struct audio_device *dev, audio_state_t new_state) +{ + struct dev_priv *priv = dev->priv; + const char *state_str; + DBusMessage *reply = NULL; + + state_str = state2str(new_state); + if (!state_str) + return; + + if (new_state == AUDIO_STATE_DISCONNECTED) { + priv->authorized = FALSE; + + if (priv->dc_id) { + device_remove_disconnect_watch(dev->btd_dev, + priv->dc_id); + priv->dc_id = 0; + } + } else if (new_state == AUDIO_STATE_CONNECTING) + priv->dc_id = device_add_disconnect_watch(dev->btd_dev, + disconnect_cb, dev, NULL); + + if (dev->priv->state == new_state) { + DBG("state change attempted from %s to %s", + state_str, state_str); + return; + } + + dev->priv->state = new_state; + + if (new_state == AUDIO_STATE_DISCONNECTED) { + if (priv->dc_req) { + reply = dbus_message_new_method_return(priv->dc_req); + dbus_message_unref(priv->dc_req); + priv->dc_req = NULL; + g_dbus_send_message(dev->conn, reply); + } + priv->disconnecting = FALSE; + } + + if (priv->conn_req && new_state != AUDIO_STATE_CONNECTING) { + if (new_state == AUDIO_STATE_CONNECTED) + reply = dbus_message_new_method_return(priv->conn_req); + else + reply = btd_error_failed(priv->conn_req, + "Connect Failed"); + + dbus_message_unref(priv->conn_req); + priv->conn_req = NULL; + g_dbus_send_message(dev->conn, reply); + } + + emit_property_changed(dev->conn, dev->path, + AUDIO_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); +} + +static gboolean avdtp_connect_timeout(gpointer user_data) +{ + struct audio_device *dev = user_data; + + dev->priv->avdtp_timer = 0; + + if (dev->sink) { + struct avdtp *session = avdtp_get(&dev->src, &dev->dst); + + if (!session) + return FALSE; + + sink_setup_stream(dev->sink, session); + avdtp_unref(session); + } + + return FALSE; +} + +static gboolean device_set_avdtp_timer(struct audio_device *dev) +{ + struct dev_priv *priv = dev->priv; + guint timeout = AVDTP_CONNECT_TIMEOUT; + + if (!dev->sink) + return FALSE; + + if (priv->avdtp_timer) + return FALSE; + + /* If the headset is the HSP/HFP RFCOMM initiator, give the headset + * time to initiate AVDTP signalling (and avoid further racing) */ + if (dev->headset && headset_get_rfcomm_initiator(dev)) + timeout += AVDTP_CONNECT_TIMEOUT_BOOST; + + priv->avdtp_timer = g_timeout_add_seconds(timeout, + avdtp_connect_timeout, + dev); + + return TRUE; +} + +static gboolean headset_connect_timeout(gpointer user_data) +{ + struct audio_device *dev = user_data; + struct dev_priv *priv = dev->priv; + + dev->priv->headset_timer = 0; + + if (dev->headset == NULL) + return FALSE; + + if (headset_config_stream(dev, FALSE, NULL, NULL) == 0) { + if (priv->state != AUDIO_STATE_CONNECTED && + (priv->sink_state == SINK_STATE_CONNECTED || + priv->sink_state == SINK_STATE_PLAYING)) + device_set_state(dev, AUDIO_STATE_CONNECTED); + } + + return FALSE; +} + +static gboolean device_set_headset_timer(struct audio_device *dev) +{ + struct dev_priv *priv = dev->priv; + + if (!dev->headset) + return FALSE; + + if (priv->headset_timer) + return FALSE; + + priv->headset_timer = g_timeout_add_seconds(HEADSET_CONNECT_TIMEOUT, + headset_connect_timeout, dev); + + return TRUE; +} + +static void device_avdtp_cb(struct audio_device *dev, struct avdtp *session, + avdtp_session_state_t old_state, + avdtp_session_state_t new_state, + void *user_data) +{ + if (!dev->sink || !dev->control) + return; + + if (new_state == AVDTP_SESSION_STATE_CONNECTED) { + if (avdtp_stream_setup_active(session)) + device_set_control_timer(dev); + else + avrcp_connect(dev); + } +} + +static void device_sink_cb(struct audio_device *dev, + sink_state_t old_state, + sink_state_t new_state, + void *user_data) +{ + struct dev_priv *priv = dev->priv; + + if (!dev->sink) + return; + + priv->sink_state = new_state; + + switch (new_state) { + case SINK_STATE_DISCONNECTED: + if (dev->control) { + device_remove_control_timer(dev); + avrcp_disconnect(dev); + } + if (priv->hs_state != HEADSET_STATE_DISCONNECTED && + (priv->dc_req || priv->disconnecting)) { + headset_shutdown(dev); + break; + } + if (priv->hs_state == HEADSET_STATE_DISCONNECTED) + device_set_state(dev, AUDIO_STATE_DISCONNECTED); + else if (old_state == SINK_STATE_CONNECTING) { + switch (priv->hs_state) { + case HEADSET_STATE_CONNECTED: + case HEADSET_STATE_PLAY_IN_PROGRESS: + case HEADSET_STATE_PLAYING: + device_set_state(dev, AUDIO_STATE_CONNECTED); + default: + break; + } + } + break; + case SINK_STATE_CONNECTING: + device_remove_avdtp_timer(dev); + if (priv->hs_state == HEADSET_STATE_DISCONNECTED) + device_set_state(dev, AUDIO_STATE_CONNECTING); + break; + case SINK_STATE_CONNECTED: + if (old_state == SINK_STATE_PLAYING) + break; + if (dev->auto_connect) { + if (!dev->headset) + device_set_state(dev, AUDIO_STATE_CONNECTED); + else if (priv->hs_state == HEADSET_STATE_DISCONNECTED) + device_set_headset_timer(dev); + else if (priv->hs_state == HEADSET_STATE_CONNECTED || + priv->hs_state == HEADSET_STATE_PLAY_IN_PROGRESS || + priv->hs_state == HEADSET_STATE_PLAYING) + device_set_state(dev, AUDIO_STATE_CONNECTED); + } else if (priv->hs_state == HEADSET_STATE_DISCONNECTED || + priv->hs_state == HEADSET_STATE_CONNECTING) + device_set_state(dev, AUDIO_STATE_CONNECTED); + break; + case SINK_STATE_PLAYING: + break; + } +} + +static void device_avctp_cb(struct audio_device *dev, + avctp_state_t old_state, + avctp_state_t new_state, + void *user_data) +{ + if (!dev->control) + return; + + dev->priv->avctp_state = new_state; + + switch (new_state) { + case AVCTP_STATE_DISCONNECTED: + break; + case AVCTP_STATE_CONNECTING: + device_remove_control_timer(dev); + break; + case AVCTP_STATE_CONNECTED: + break; + } +} + +static void device_headset_cb(struct audio_device *dev, + headset_state_t old_state, + headset_state_t new_state, + void *user_data) +{ + struct dev_priv *priv = dev->priv; + + if (!dev->headset) + return; + + priv->hs_state = new_state; + + switch (new_state) { + case HEADSET_STATE_DISCONNECTED: + device_remove_avdtp_timer(dev); + if (priv->sink_state != SINK_STATE_DISCONNECTED && dev->sink && + (priv->dc_req || priv->disconnecting)) { + sink_shutdown(dev->sink); + break; + } + if (priv->sink_state == SINK_STATE_DISCONNECTED) + device_set_state(dev, AUDIO_STATE_DISCONNECTED); + else if (old_state == HEADSET_STATE_CONNECTING && + (priv->sink_state == SINK_STATE_CONNECTED || + priv->sink_state == SINK_STATE_PLAYING)) + device_set_state(dev, AUDIO_STATE_CONNECTED); + break; + case HEADSET_STATE_CONNECTING: + device_remove_headset_timer(dev); + if (priv->sink_state == SINK_STATE_DISCONNECTED) + device_set_state(dev, AUDIO_STATE_CONNECTING); + break; + case HEADSET_STATE_CONNECTED: + if (old_state == HEADSET_STATE_CONNECTED || + old_state == HEADSET_STATE_PLAY_IN_PROGRESS || + old_state == HEADSET_STATE_PLAYING) + break; + if (dev->auto_connect) { + if (!dev->sink) + device_set_state(dev, AUDIO_STATE_CONNECTED); + else if (priv->sink_state == SINK_STATE_DISCONNECTED) + device_set_avdtp_timer(dev); + else if (priv->sink_state == SINK_STATE_CONNECTED || + priv->sink_state == SINK_STATE_PLAYING) + device_set_state(dev, AUDIO_STATE_CONNECTED); + } else if (priv->sink_state == SINK_STATE_DISCONNECTED || + priv->sink_state == SINK_STATE_CONNECTING) + device_set_state(dev, AUDIO_STATE_CONNECTED); + break; + case HEADSET_STATE_PLAY_IN_PROGRESS: + break; + case HEADSET_STATE_PLAYING: + break; + } +} + +static DBusMessage *dev_connect(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *dev = data; + struct dev_priv *priv = dev->priv; + + if (priv->state == AUDIO_STATE_CONNECTING) + return btd_error_in_progress(msg); + else if (priv->state == AUDIO_STATE_CONNECTED) + return btd_error_already_connected(msg); + + dev->auto_connect = TRUE; + + if (dev->headset) + headset_config_stream(dev, FALSE, NULL, NULL); + + if (priv->state != AUDIO_STATE_CONNECTING && dev->sink) { + struct avdtp *session = avdtp_get(&dev->src, &dev->dst); + + if (!session) + return btd_error_failed(msg, + "Failed to get AVDTP session"); + + sink_setup_stream(dev->sink, session); + avdtp_unref(session); + } + + /* The previous calls should cause a call to the state callback to + * indicate AUDIO_STATE_CONNECTING */ + if (priv->state != AUDIO_STATE_CONNECTING) + return btd_error_failed(msg, "Connect Failed"); + + priv->conn_req = dbus_message_ref(msg); + + return NULL; +} + +static DBusMessage *dev_disconnect(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *dev = data; + struct dev_priv *priv = dev->priv; + + if (priv->state == AUDIO_STATE_DISCONNECTED) + return btd_error_not_connected(msg); + + if (priv->dc_req) + return dbus_message_new_method_return(msg); + + priv->dc_req = dbus_message_ref(msg); + + if (dev->control) { + device_remove_control_timer(dev); + avrcp_disconnect(dev); + } + + if (dev->sink && priv->sink_state != SINK_STATE_DISCONNECTED) + sink_shutdown(dev->sink); + else if (priv->hs_state != HEADSET_STATE_DISCONNECTED) + headset_shutdown(dev); + else { + dbus_message_unref(priv->dc_req); + priv->dc_req = NULL; + return dbus_message_new_method_return(msg); + } + + return NULL; +} + +static DBusMessage *dev_get_properties(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + const char *state; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + /* State */ + state = state2str(device->priv->state); + if (state) + dict_append_entry(&dict, "State", DBUS_TYPE_STRING, &state); + + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static const GDBusMethodTable dev_methods[] = { + { GDBUS_ASYNC_METHOD("Connect", NULL, NULL, dev_connect) }, + { GDBUS_METHOD("Disconnect", NULL, NULL, dev_disconnect) }, + { GDBUS_METHOD("GetProperties", + NULL, GDBUS_ARGS({ "properties", "a{sv}" }), + dev_get_properties) }, + { } +}; + +static const GDBusSignalTable dev_signals[] = { + { GDBUS_SIGNAL("PropertyChanged", + GDBUS_ARGS({ "name", "s" }, { "value", "v" })) }, + { } +}; + +struct audio_device *audio_device_register(DBusConnection *conn, + struct btd_device *device, + const char *path, const bdaddr_t *src, + const bdaddr_t *dst) +{ + struct audio_device *dev; + + if (!conn || !path) + return NULL; + + dev = g_new0(struct audio_device, 1); + + dev->btd_dev = btd_device_ref(device); + dev->path = g_strdup(path); + bacpy(&dev->dst, dst); + bacpy(&dev->src, src); + dev->conn = dbus_connection_ref(conn); + dev->priv = g_new0(struct dev_priv, 1); + dev->priv->state = AUDIO_STATE_DISCONNECTED; + + if (!g_dbus_register_interface(dev->conn, dev->path, + AUDIO_INTERFACE, + dev_methods, dev_signals, NULL, + dev, NULL)) { + error("Unable to register %s on %s", AUDIO_INTERFACE, + dev->path); + device_free(dev); + return NULL; + } + + DBG("Registered interface %s on path %s", AUDIO_INTERFACE, + dev->path); + + if (sink_callback_id == 0) + sink_callback_id = sink_add_state_cb(device_sink_cb, NULL); + + if (avdtp_callback_id == 0) + avdtp_callback_id = avdtp_add_state_cb(device_avdtp_cb, NULL); + if (avctp_callback_id == 0) + avctp_callback_id = avctp_add_state_cb(device_avctp_cb, NULL); + + if (headset_callback_id == 0) + headset_callback_id = headset_add_state_cb(device_headset_cb, + NULL); + + return dev; +} + +gboolean audio_device_is_active(struct audio_device *dev, + const char *interface) +{ + if (!interface) { + if ((dev->sink || dev->source) && + avdtp_is_connected(&dev->src, &dev->dst)) + return TRUE; + if (dev->headset && headset_is_active(dev)) + return TRUE; + } else if (!strcmp(interface, AUDIO_SINK_INTERFACE) && dev->sink && + avdtp_is_connected(&dev->src, &dev->dst)) + return TRUE; + else if (!strcmp(interface, AUDIO_SOURCE_INTERFACE) && dev->source && + avdtp_is_connected(&dev->src, &dev->dst)) + return TRUE; + else if (!strcmp(interface, AUDIO_HEADSET_INTERFACE) && dev->headset && + headset_is_active(dev)) + return TRUE; + else if (!strcmp(interface, AUDIO_CONTROL_INTERFACE) && dev->control && + control_is_active(dev)) + return TRUE; + else if (!strcmp(interface, AUDIO_GATEWAY_INTERFACE) && dev->gateway && + gateway_is_active(dev)) + return TRUE; + + return FALSE; +} + +void audio_device_unregister(struct audio_device *device) +{ + unix_device_removed(device); + + if (device->hs_preauth_id) { + g_source_remove(device->hs_preauth_id); + device->hs_preauth_id = 0; + } + + if (device->headset) + headset_unregister(device); + + if (device->gateway) + gateway_unregister(device); + + if (device->sink) + sink_unregister(device); + + if (device->source) + source_unregister(device); + + if (device->control) + control_unregister(device); + + g_dbus_unregister_interface(device->conn, device->path, + AUDIO_INTERFACE); + + device_free(device); +} + +static void auth_cb(DBusError *derr, void *user_data) +{ + struct audio_device *dev = user_data; + struct dev_priv *priv = dev->priv; + + if (derr == NULL) + priv->authorized = TRUE; + + while (priv->auths) { + struct service_auth *auth = priv->auths->data; + + auth->cb(derr, auth->user_data); + priv->auths = g_slist_remove(priv->auths, auth); + g_free(auth); + } +} + +static gboolean auth_idle_cb(gpointer user_data) +{ + struct audio_device *dev = user_data; + struct dev_priv *priv = dev->priv; + + priv->auth_idle_id = 0; + + auth_cb(NULL, dev); + + return FALSE; +} + +static gboolean audio_device_is_connected(struct audio_device *dev) +{ + if (dev->headset) { + headset_state_t state = headset_get_state(dev); + + if (state == HEADSET_STATE_CONNECTED || + state == HEADSET_STATE_PLAY_IN_PROGRESS || + state == HEADSET_STATE_PLAYING) + return TRUE; + } + + if (dev->sink) { + sink_state_t state = sink_get_state(dev); + + if (state == SINK_STATE_CONNECTED || + state == SINK_STATE_PLAYING) + return TRUE; + } + + if (dev->source) { + source_state_t state = source_get_state(dev); + + if (state == SOURCE_STATE_CONNECTED || + state == SOURCE_STATE_PLAYING) + return TRUE; + } + + return FALSE; +} + +int audio_device_request_authorization(struct audio_device *dev, + const char *uuid, service_auth_cb cb, + void *user_data) +{ + struct dev_priv *priv = dev->priv; + struct service_auth *auth; + int err; + + auth = g_try_new0(struct service_auth, 1); + if (!auth) + return -ENOMEM; + + auth->cb = cb; + auth->user_data = user_data; + + priv->auths = g_slist_append(priv->auths, auth); + if (g_slist_length(priv->auths) > 1) + return 0; + + if (priv->authorized || audio_device_is_connected(dev)) { + priv->auth_idle_id = g_idle_add(auth_idle_cb, dev); + return 0; + } + + err = btd_request_authorization(&dev->src, &dev->dst, uuid, auth_cb, + dev); + if (err < 0) { + priv->auths = g_slist_remove(priv->auths, auth); + g_free(auth); + } + + return err; +} + +int audio_device_cancel_authorization(struct audio_device *dev, + authorization_cb cb, void *user_data) +{ + struct dev_priv *priv = dev->priv; + GSList *l, *next; + + for (l = priv->auths; l != NULL; l = next) { + struct service_auth *auth = l->data; + + next = g_slist_next(l); + + if (cb && auth->cb != cb) + continue; + + if (user_data && auth->user_data != user_data) + continue; + + priv->auths = g_slist_remove(priv->auths, auth); + g_free(auth); + } + + if (g_slist_length(priv->auths) == 0) { + if (priv->auth_idle_id > 0) { + g_source_remove(priv->auth_idle_id); + priv->auth_idle_id = 0; + } else + btd_cancel_authorization(&dev->src, &dev->dst); + } + + return 0; +} + +void audio_device_set_authorized(struct audio_device *dev, gboolean auth) +{ + struct dev_priv *priv = dev->priv; + + priv->authorized = auth; +} diff --git a/audio/device.h b/audio/device.h new file mode 100644 index 0000000..75f1da9 --- /dev/null +++ b/audio/device.h @@ -0,0 +1,74 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +struct source; +struct control; +struct target; +struct sink; +struct headset; +struct gateway; +struct dev_priv; + +struct audio_device { + struct btd_device *btd_dev; + + DBusConnection *conn; + char *path; + bdaddr_t src; + bdaddr_t dst; + + gboolean auto_connect; + + struct headset *headset; + struct gateway *gateway; + struct sink *sink; + struct source *source; + struct control *control; + struct target *target; + + guint hs_preauth_id; + + struct dev_priv *priv; +}; + +struct audio_device *audio_device_register(DBusConnection *conn, + struct btd_device *device, + const char *path, const bdaddr_t *src, + const bdaddr_t *dst); + +void audio_device_unregister(struct audio_device *device); + +gboolean audio_device_is_active(struct audio_device *dev, + const char *interface); + +typedef void (*authorization_cb) (DBusError *derr, void *user_data); + +int audio_device_cancel_authorization(struct audio_device *dev, + authorization_cb cb, void *user_data); + +int audio_device_request_authorization(struct audio_device *dev, + const char *uuid, authorization_cb cb, + void *user_data); + +void audio_device_set_authorized(struct audio_device *dev, gboolean auth); diff --git a/audio/gateway.c b/audio/gateway.c new file mode 100644 index 0000000..6162948 --- /dev/null +++ b/audio/gateway.c @@ -0,0 +1,997 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * Copyright (C) 2008-2009 Leonid Movshovich + * Copyright (C) 2010 ProFUSION embedded systems + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "sdp-client.h" +#include "device.h" +#include "gateway.h" +#include "log.h" +#include "error.h" +#include "btio.h" +#include "dbus-common.h" + +struct hf_agent { + char *name; /* Bus id */ + char *path; /* D-Bus path */ + guint watch; /* Disconnect watch */ +}; + +struct connect_cb { + unsigned int id; + gateway_stream_cb_t cb; + void *cb_data; +}; + +struct gateway { + gateway_state_t state; + GIOChannel *rfcomm; + GIOChannel *sco; + GIOChannel *incoming; + GSList *callbacks; + struct hf_agent *agent; + DBusMessage *msg; + int version; + gateway_lock_t lock; +}; + +struct gateway_state_callback { + gateway_state_cb cb; + void *user_data; + unsigned int id; +}; + +static GSList *gateway_callbacks = NULL; + +int gateway_close(struct audio_device *device); + +GQuark gateway_error_quark(void) +{ + return g_quark_from_static_string("gateway-error-quark"); +} + +static const char *state2str(gateway_state_t state) +{ + switch (state) { + case GATEWAY_STATE_DISCONNECTED: + return "disconnected"; + case GATEWAY_STATE_CONNECTING: + return "connecting"; + case GATEWAY_STATE_CONNECTED: + return "connected"; + case GATEWAY_STATE_PLAYING: + return "playing"; + default: + return ""; + } +} + +static void agent_free(struct hf_agent *agent) +{ + if (!agent) + return; + + g_free(agent->name); + g_free(agent->path); + g_free(agent); +} + +static void change_state(struct audio_device *dev, gateway_state_t new_state) +{ + struct gateway *gw = dev->gateway; + const char *val; + GSList *l; + gateway_state_t old_state; + + if (gw->state == new_state) + return; + + val = state2str(new_state); + old_state = gw->state; + gw->state = new_state; + + emit_property_changed(dev->conn, dev->path, + AUDIO_GATEWAY_INTERFACE, "State", + DBUS_TYPE_STRING, &val); + + for (l = gateway_callbacks; l != NULL; l = l->next) { + struct gateway_state_callback *cb = l->data; + cb->cb(dev, old_state, new_state, cb->user_data); + } +} + +void gateway_set_state(struct audio_device *dev, gateway_state_t new_state) +{ + switch (new_state) { + case GATEWAY_STATE_DISCONNECTED: + gateway_close(dev); + break; + case GATEWAY_STATE_CONNECTING: + case GATEWAY_STATE_CONNECTED: + case GATEWAY_STATE_PLAYING: + break; + } +} + +static void agent_disconnect(struct audio_device *dev, struct hf_agent *agent) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(agent->name, agent->path, + "org.bluez.HandsfreeAgent", "Release"); + + g_dbus_send_message(dev->conn, msg); +} + +static gboolean agent_sendfd(struct hf_agent *agent, int fd, + DBusPendingCallNotifyFunction notify, void *data) +{ + struct audio_device *dev = data; + struct gateway *gw = dev->gateway; + DBusMessage *msg; + DBusPendingCall *call; + + msg = dbus_message_new_method_call(agent->name, agent->path, + "org.bluez.HandsfreeAgent", "NewConnection"); + + dbus_message_append_args(msg, DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_UINT16, &gw->version, + DBUS_TYPE_INVALID); + + if (dbus_connection_send_with_reply(dev->conn, msg, + &call, -1) == FALSE) { + dbus_message_unref(msg); + return FALSE; + } + + dbus_pending_call_set_notify(call, notify, dev, NULL); + dbus_pending_call_unref(call); + dbus_message_unref(msg); + + return TRUE; +} + +static unsigned int connect_cb_new(struct gateway *gw, + gateway_stream_cb_t func, + void *user_data) +{ + struct connect_cb *cb; + static unsigned int free_cb_id = 1; + + if (!func) + return 0; + + cb = g_new(struct connect_cb, 1); + + cb->cb = func; + cb->cb_data = user_data; + cb->id = free_cb_id++; + + gw->callbacks = g_slist_append(gw->callbacks, cb); + + return cb->id; +} + +static void run_connect_cb(struct audio_device *dev, GError *err) +{ + struct gateway *gw = dev->gateway; + GSList *l; + + for (l = gw->callbacks; l != NULL; l = l->next) { + struct connect_cb *cb = l->data; + cb->cb(dev, err, cb->cb_data); + } + + g_slist_free_full(gw->callbacks, g_free); + gw->callbacks = NULL; +} + +static gboolean sco_io_cb(GIOChannel *chan, GIOCondition cond, + struct audio_device *dev) +{ + struct gateway *gw = dev->gateway; + + if (cond & G_IO_NVAL) + return FALSE; + + DBG("sco connection is released"); + g_io_channel_shutdown(gw->sco, TRUE, NULL); + g_io_channel_unref(gw->sco); + gw->sco = NULL; + change_state(dev, GATEWAY_STATE_CONNECTED); + + return FALSE; +} + +static void sco_connect_cb(GIOChannel *chan, GError *err, gpointer user_data) +{ + struct audio_device *dev = (struct audio_device *) user_data; + struct gateway *gw = dev->gateway; + + DBG("at the begin of sco_connect_cb() in gateway.c"); + + gw->sco = g_io_channel_ref(chan); + + if (err) { + error("sco_connect_cb(): %s", err->message); + gateway_suspend_stream(dev); + return; + } + + g_io_add_watch(gw->sco, G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) sco_io_cb, dev); + + change_state(dev, GATEWAY_STATE_PLAYING); + run_connect_cb(dev, NULL); +} + +static gboolean rfcomm_disconnect_cb(GIOChannel *chan, GIOCondition cond, + struct audio_device *dev) +{ + if (cond & G_IO_NVAL) + return FALSE; + + gateway_close(dev); + + return FALSE; +} + +static void newconnection_reply(DBusPendingCall *call, void *data) +{ + struct audio_device *dev = data; + struct gateway *gw = dev->gateway; + DBusMessage *reply = dbus_pending_call_steal_reply(call); + DBusError derr; + + if (!dev->gateway->rfcomm) { + DBG("RFCOMM disconnected from server before agent reply"); + goto done; + } + + dbus_error_init(&derr); + if (!dbus_set_error_from_message(&derr, reply)) { + DBG("Agent reply: file descriptor passed successfully"); + g_io_add_watch(gw->rfcomm, G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) rfcomm_disconnect_cb, dev); + change_state(dev, GATEWAY_STATE_CONNECTED); + goto done; + } + + DBG("Agent reply: %s", derr.message); + + dbus_error_free(&derr); + gateway_close(dev); + +done: + dbus_message_unref(reply); +} + +static void rfcomm_connect_cb(GIOChannel *chan, GError *err, + gpointer user_data) +{ + struct audio_device *dev = user_data; + struct gateway *gw = dev->gateway; + DBusMessage *reply; + int sk, ret; + + if (err) { + error("connect(): %s", err->message); + goto fail; + } + + if (!gw->agent) { + error("Handsfree Agent not registered"); + goto fail; + } + + sk = g_io_channel_unix_get_fd(chan); + + if (gw->rfcomm == NULL) + gw->rfcomm = g_io_channel_ref(chan); + + ret = agent_sendfd(gw->agent, sk, newconnection_reply, dev); + + if (!gw->msg) + return; + + if (ret) + reply = dbus_message_new_method_return(gw->msg); + else + reply = btd_error_failed(gw->msg, "Can't pass file descriptor"); + + g_dbus_send_message(dev->conn, reply); + + return; + +fail: + if (gw->msg) { + DBusMessage *reply; + reply = btd_error_failed(gw->msg, "Connect failed"); + g_dbus_send_message(dev->conn, reply); + } + + gateway_close(dev); +} + +static int get_remote_profile_version(sdp_record_t *rec) +{ + uuid_t uuid; + sdp_list_t *profiles; + sdp_profile_desc_t *desc; + int ver = 0; + + sdp_uuid16_create(&uuid, HANDSFREE_PROFILE_ID); + + sdp_get_profile_descs(rec, &profiles); + if (profiles == NULL) + goto done; + + desc = profiles->data; + + if (sdp_uuid16_cmp(&desc->uuid, &uuid) == 0) + ver = desc->version; + + sdp_list_free(profiles, free); + +done: + return ver; +} + +static void get_incoming_record_cb(sdp_list_t *recs, int err, + gpointer user_data) +{ + struct audio_device *dev = user_data; + struct gateway *gw = dev->gateway; + GError *gerr = NULL; + + if (err < 0) { + error("Unable to get service record: %s (%d)", strerror(-err), + -err); + goto fail; + } + + if (!recs || !recs->data) { + error("No records found"); + goto fail; + } + + gw->version = get_remote_profile_version(recs->data); + if (gw->version == 0) + goto fail; + + rfcomm_connect_cb(gw->incoming, gerr, dev); + return; + +fail: + gateway_close(dev); +} + +static void unregister_incoming(gpointer user_data) +{ + struct audio_device *dev = user_data; + struct gateway *gw = dev->gateway; + + if (gw->incoming) { + g_io_channel_unref(gw->incoming); + gw->incoming = NULL; + } +} + +static void rfcomm_incoming_cb(GIOChannel *chan, GError *err, + gpointer user_data) +{ + struct audio_device *dev = user_data; + struct gateway *gw = dev->gateway; + uuid_t uuid; + + gw->incoming = g_io_channel_ref(chan); + + sdp_uuid16_create(&uuid, HANDSFREE_AGW_SVCLASS_ID); + if (bt_search_service(&dev->src, &dev->dst, &uuid, + get_incoming_record_cb, dev, + unregister_incoming) == 0) + return; + + unregister_incoming(dev); + gateway_close(dev); +} + +static void get_record_cb(sdp_list_t *recs, int err, gpointer user_data) +{ + struct audio_device *dev = user_data; + struct gateway *gw = dev->gateway; + int ch; + sdp_list_t *protos, *classes; + uuid_t uuid; + GIOChannel *io; + GError *gerr = NULL; + + if (err < 0) { + error("Unable to get service record: %s (%d)", strerror(-err), + -err); + goto fail; + } + + if (!recs || !recs->data) { + error("No records found"); + err = -EIO; + goto fail; + } + + if (sdp_get_service_classes(recs->data, &classes) < 0) { + error("Unable to get service classes from record"); + err = -EINVAL; + goto fail; + } + + if (sdp_get_access_protos(recs->data, &protos) < 0) { + error("Unable to get access protocols from record"); + err = -ENODATA; + goto fail; + } + + gw->version = get_remote_profile_version(recs->data); + if (gw->version == 0) { + error("Unable to get profile version from record"); + err = -EINVAL; + goto fail; + } + + memcpy(&uuid, classes->data, sizeof(uuid)); + sdp_list_free(classes, free); + + if (!sdp_uuid128_to_uuid(&uuid) || uuid.type != SDP_UUID16 || + uuid.value.uuid16 != HANDSFREE_AGW_SVCLASS_ID) { + sdp_list_free(protos, NULL); + error("Invalid service record or not HFP"); + err = -EIO; + goto fail; + } + + ch = sdp_get_proto_port(protos, RFCOMM_UUID); + sdp_list_foreach(protos, (sdp_list_func_t) sdp_list_free, NULL); + sdp_list_free(protos, NULL); + if (ch <= 0) { + error("Unable to extract RFCOMM channel from service record"); + err = -EIO; + goto fail; + } + + io = bt_io_connect(BT_IO_RFCOMM, rfcomm_connect_cb, dev, NULL, &gerr, + BT_IO_OPT_SOURCE_BDADDR, &dev->src, + BT_IO_OPT_DEST_BDADDR, &dev->dst, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_CHANNEL, ch, + BT_IO_OPT_INVALID); + if (!io) { + error("Unable to connect: %s", gerr->message); + goto fail; + } + + g_io_channel_unref(io); + return; + +fail: + if (gw->msg) { + DBusMessage *reply = btd_error_failed(gw->msg, + gerr ? gerr->message : strerror(-err)); + g_dbus_send_message(dev->conn, reply); + } + + gateway_close(dev); + + if (gerr) + g_error_free(gerr); +} + +static int get_records(struct audio_device *device) +{ + uuid_t uuid; + + change_state(device, GATEWAY_STATE_CONNECTING); + sdp_uuid16_create(&uuid, HANDSFREE_AGW_SVCLASS_ID); + return bt_search_service(&device->src, &device->dst, &uuid, + get_record_cb, device, NULL); +} + +static DBusMessage *ag_connect(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *au_dev = (struct audio_device *) data; + struct gateway *gw = au_dev->gateway; + int err; + + if (!gw->agent) + return btd_error_agent_not_available(msg); + + err = get_records(au_dev); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + gw->msg = dbus_message_ref(msg); + + return NULL; +} + +int gateway_close(struct audio_device *device) +{ + GError *gerr = NULL; + struct gateway *gw = device->gateway; + int sock; + + if (gw->rfcomm) { + sock = g_io_channel_unix_get_fd(gw->rfcomm); + shutdown(sock, SHUT_RDWR); + + g_io_channel_shutdown(gw->rfcomm, TRUE, NULL); + g_io_channel_unref(gw->rfcomm); + gw->rfcomm = NULL; + } + + if (gw->sco) { + g_io_channel_shutdown(gw->sco, TRUE, NULL); + g_io_channel_unref(gw->sco); + gw->sco = NULL; + } + + change_state(device, GATEWAY_STATE_DISCONNECTED); + g_set_error(&gerr, GATEWAY_ERROR, + GATEWAY_ERROR_DISCONNECTED, "Disconnected"); + run_connect_cb(device, gerr); + g_error_free(gerr); + + return 0; +} + +static DBusMessage *ag_disconnect(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct gateway *gw = device->gateway; + DBusMessage *reply = NULL; + char gw_addr[18]; + + if (!device->conn) + return NULL; + + if (!gw->rfcomm) + return btd_error_not_connected(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + gateway_close(device); + ba2str(&device->dst, gw_addr); + DBG("Disconnected from %s, %s", gw_addr, device->path); + + return reply; +} + +static void agent_exited(DBusConnection *conn, void *data) +{ + struct gateway *gateway = data; + struct hf_agent *agent = gateway->agent; + + DBG("Agent %s exited", agent->name); + + agent_free(agent); + gateway->agent = NULL; +} + +static DBusMessage *ag_get_properties(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct gateway *gw = device->gateway; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + const char *value; + + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + value = state2str(gw->state); + dict_append_entry(&dict, "State", + DBUS_TYPE_STRING, &value); + + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static DBusMessage *register_agent(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + struct gateway *gw = device->gateway; + struct hf_agent *agent; + const char *path, *name; + + if (gw->agent) + return btd_error_already_exists(msg); + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + name = dbus_message_get_sender(msg); + agent = g_new0(struct hf_agent, 1); + + agent->name = g_strdup(name); + agent->path = g_strdup(path); + + agent->watch = g_dbus_add_disconnect_watch(conn, name, + agent_exited, gw, NULL); + + gw->agent = agent; + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *unregister_agent(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + struct gateway *gw = device->gateway; + const char *path; + + if (!gw->agent) + goto done; + + if (strcmp(gw->agent->name, dbus_message_get_sender(msg)) != 0) + return btd_error_not_authorized(msg); + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + if (strcmp(gw->agent->path, path) != 0) + return btd_error_does_not_exist(msg); + + g_dbus_remove_watch(device->conn, gw->agent->watch); + + agent_free(gw->agent); + gw->agent = NULL; + +done: + return dbus_message_new_method_return(msg); +} + +static const GDBusMethodTable gateway_methods[] = { + { GDBUS_ASYNC_METHOD("Connect", NULL, NULL, ag_connect) }, + { GDBUS_ASYNC_METHOD("Disconnect", NULL, NULL, ag_disconnect) }, + { GDBUS_METHOD("GetProperties", + NULL, GDBUS_ARGS({ "properties", "a{sv}" }), + ag_get_properties) }, + { GDBUS_METHOD("RegisterAgent", + GDBUS_ARGS({ "agent", "o" }), NULL, register_agent) }, + { GDBUS_METHOD("UnregisterAgent", + GDBUS_ARGS({ "agent", "o" }), NULL, unregister_agent) }, + { } +}; + +static const GDBusSignalTable gateway_signals[] = { + { GDBUS_SIGNAL("PropertyChanged", + GDBUS_ARGS({ "name", "s" }, { "value", "v" })) }, + { } +}; + +static void path_unregister(void *data) +{ + struct audio_device *dev = data; + + DBG("Unregistered interface %s on path %s", + AUDIO_GATEWAY_INTERFACE, dev->path); + + gateway_close(dev); + + g_free(dev->gateway); + dev->gateway = NULL; +} + +void gateway_unregister(struct audio_device *dev) +{ + if (dev->gateway->agent) + agent_disconnect(dev, dev->gateway->agent); + + g_dbus_unregister_interface(dev->conn, dev->path, + AUDIO_GATEWAY_INTERFACE); +} + +struct gateway *gateway_init(struct audio_device *dev) +{ + if (!g_dbus_register_interface(dev->conn, dev->path, + AUDIO_GATEWAY_INTERFACE, + gateway_methods, gateway_signals, + NULL, dev, path_unregister)) + return NULL; + + return g_new0(struct gateway, 1); +} + +gboolean gateway_is_connected(struct audio_device *dev) +{ + struct gateway *gw = dev->gateway; + + if (gw->state == GATEWAY_STATE_CONNECTED) + return TRUE; + + return FALSE; +} + +gboolean gateway_is_active(struct audio_device *dev) +{ + struct gateway *gw = dev->gateway; + + if (gw->state != GATEWAY_STATE_DISCONNECTED) + return TRUE; + + return FALSE; +} + +int gateway_connect_rfcomm(struct audio_device *dev, GIOChannel *io) +{ + if (!io) + return -EINVAL; + + dev->gateway->rfcomm = g_io_channel_ref(io); + + change_state(dev, GATEWAY_STATE_CONNECTING); + + return 0; +} + +int gateway_connect_sco(struct audio_device *dev, GIOChannel *io) +{ + struct gateway *gw = dev->gateway; + + if (gw->sco) + return -EISCONN; + + gw->sco = g_io_channel_ref(io); + + g_io_add_watch(gw->sco, G_IO_ERR | G_IO_HUP | G_IO_NVAL, + (GIOFunc) sco_io_cb, dev); + + change_state(dev, GATEWAY_STATE_PLAYING); + + return 0; +} + +void gateway_start_service(struct audio_device *dev) +{ + struct gateway *gw = dev->gateway; + GError *err = NULL; + + if (gw->rfcomm == NULL) + return; + + if (!bt_io_accept(gw->rfcomm, rfcomm_incoming_cb, dev, NULL, &err)) { + error("bt_io_accept: %s", err->message); + g_error_free(err); + gateway_close(dev); + } +} + +static gboolean request_stream_cb(gpointer data) +{ + run_connect_cb(data, NULL); + return FALSE; +} + +/* These are functions to be called from unix.c for audio system + * ifaces (alsa, gstreamer, etc.) */ +unsigned int gateway_request_stream(struct audio_device *dev, + gateway_stream_cb_t cb, void *user_data) +{ + struct gateway *gw = dev->gateway; + GError *err = NULL; + GIOChannel *io; + + if (!gw->rfcomm) + get_records(dev); + else if (!gw->sco) { + io = bt_io_connect(BT_IO_SCO, sco_connect_cb, dev, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &dev->src, + BT_IO_OPT_DEST_BDADDR, &dev->dst, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_INVALID); + if (!io) { + error("%s", err->message); + g_error_free(err); + return 0; + } + } else + g_idle_add(request_stream_cb, dev); + + return connect_cb_new(gw, cb, user_data); +} + +int gateway_config_stream(struct audio_device *dev, gateway_stream_cb_t cb, + void *user_data) +{ + struct gateway *gw = dev->gateway; + unsigned int id; + + id = connect_cb_new(gw, cb, user_data); + + if (!gw->rfcomm) + get_records(dev); + else if (cb) + g_idle_add(request_stream_cb, dev); + + return id; +} + +gboolean gateway_cancel_stream(struct audio_device *dev, unsigned int id) +{ + struct gateway *gw = dev->gateway; + GSList *l; + struct connect_cb *cb = NULL; + + for (l = gw->callbacks; l != NULL; l = l->next) { + struct connect_cb *tmp = l->data; + + if (tmp->id == id) { + cb = tmp; + break; + } + } + + if (!cb) + return FALSE; + + gw->callbacks = g_slist_remove(gw->callbacks, cb); + g_free(cb); + + gateway_suspend_stream(dev); + + return TRUE; +} + +int gateway_get_sco_fd(struct audio_device *dev) +{ + struct gateway *gw = dev->gateway; + + if (!gw || !gw->sco) + return -1; + + return g_io_channel_unix_get_fd(gw->sco); +} + +void gateway_suspend_stream(struct audio_device *dev) +{ + GError *gerr = NULL; + struct gateway *gw = dev->gateway; + + if (!gw || !gw->sco) + return; + + g_io_channel_shutdown(gw->sco, TRUE, NULL); + g_io_channel_unref(gw->sco); + gw->sco = NULL; + g_set_error(&gerr, GATEWAY_ERROR, GATEWAY_ERROR_SUSPENDED, "Suspended"); + run_connect_cb(dev, gerr); + g_error_free(gerr); + change_state(dev, GATEWAY_STATE_CONNECTED); +} + +unsigned int gateway_add_state_cb(gateway_state_cb cb, void *user_data) +{ + struct gateway_state_callback *state_cb; + static unsigned int id = 0; + + state_cb = g_new(struct gateway_state_callback, 1); + state_cb->cb = cb; + state_cb->user_data = user_data; + state_cb->id = ++id; + + gateway_callbacks = g_slist_append(gateway_callbacks, state_cb); + + return state_cb->id; +} + +gboolean gateway_remove_state_cb(unsigned int id) +{ + GSList *l; + + for (l = gateway_callbacks; l != NULL; l = l->next) { + struct gateway_state_callback *cb = l->data; + if (cb && cb->id == id) { + gateway_callbacks = g_slist_remove(gateway_callbacks, + cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} + +gateway_lock_t gateway_get_lock(struct audio_device *dev) +{ + struct gateway *gw = dev->gateway; + + return gw->lock; +} + +gboolean gateway_lock(struct audio_device *dev, gateway_lock_t lock) +{ + struct gateway *gw = dev->gateway; + + if (gw->lock & lock) + return FALSE; + + gw->lock |= lock; + + return TRUE; +} + +gboolean gateway_unlock(struct audio_device *dev, gateway_lock_t lock) +{ + struct gateway *gw = dev->gateway; + + if (!(gw->lock & lock)) + return FALSE; + + gw->lock &= ~lock; + + if (gw->lock) + return TRUE; + + if (gw->state == GATEWAY_STATE_PLAYING) + gateway_suspend_stream(dev); + + return TRUE; +} diff --git a/audio/gateway.h b/audio/gateway.h new file mode 100644 index 0000000..77f5787 --- /dev/null +++ b/audio/gateway.h @@ -0,0 +1,76 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define AUDIO_GATEWAY_INTERFACE "org.bluez.HandsfreeGateway" + +#define DEFAULT_HFP_HS_CHANNEL 7 + +typedef enum { + GATEWAY_STATE_DISCONNECTED, + GATEWAY_STATE_CONNECTING, + GATEWAY_STATE_CONNECTED, + GATEWAY_STATE_PLAYING, +} gateway_state_t; + +typedef enum { + GATEWAY_LOCK_READ = 1, + GATEWAY_LOCK_WRITE = 1 << 1, +} gateway_lock_t; + +typedef enum { + GATEWAY_ERROR_DISCONNECTED, + GATEWAY_ERROR_SUSPENDED, +} gateway_error_t; + +#define GATEWAY_ERROR gateway_error_quark() + +GQuark gateway_error_quark(void); + +typedef void (*gateway_state_cb) (struct audio_device *dev, + gateway_state_t old_state, + gateway_state_t new_state, + void *user_data); +typedef void (*gateway_stream_cb_t) (struct audio_device *dev, GError *err, + void *user_data); + +void gateway_set_state(struct audio_device *dev, gateway_state_t new_state); +void gateway_unregister(struct audio_device *dev); +struct gateway *gateway_init(struct audio_device *device); +gboolean gateway_is_active(struct audio_device *dev); +gboolean gateway_is_connected(struct audio_device *dev); +int gateway_connect_rfcomm(struct audio_device *dev, GIOChannel *io); +int gateway_connect_sco(struct audio_device *dev, GIOChannel *chan); +void gateway_start_service(struct audio_device *device); +unsigned int gateway_request_stream(struct audio_device *dev, + gateway_stream_cb_t cb, void *user_data); +int gateway_config_stream(struct audio_device *dev, gateway_stream_cb_t cb, + void *user_data); +gboolean gateway_cancel_stream(struct audio_device *dev, unsigned int id); +int gateway_get_sco_fd(struct audio_device *dev); +void gateway_suspend_stream(struct audio_device *dev); +unsigned int gateway_add_state_cb(gateway_state_cb cb, void *user_data); +gboolean gateway_remove_state_cb(unsigned int id); +gateway_lock_t gateway_get_lock(struct audio_device *dev); +gboolean gateway_lock(struct audio_device *dev, gateway_lock_t lock); +gboolean gateway_unlock(struct audio_device *dev, gateway_lock_t lock); diff --git a/audio/gsta2dpsink.c b/audio/gsta2dpsink.c new file mode 100644 index 0000000..c8f6346 --- /dev/null +++ b/audio/gsta2dpsink.c @@ -0,0 +1,729 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "gstpragma.h" +#include "gsta2dpsink.h" + +GST_DEBUG_CATEGORY_STATIC(gst_a2dp_sink_debug); +#define GST_CAT_DEFAULT gst_a2dp_sink_debug + +#define A2DP_SBC_RTP_PAYLOAD_TYPE 1 +#define TEMPLATE_MAX_BITPOOL_STR "64" + +#define DEFAULT_AUTOCONNECT TRUE + +enum { + PROP_0, + PROP_DEVICE, + PROP_AUTOCONNECT, + PROP_TRANSPORT +}; + +GST_BOILERPLATE(GstA2dpSink, gst_a2dp_sink, GstBin, GST_TYPE_BIN); + +static const GstElementDetails gst_a2dp_sink_details = + GST_ELEMENT_DETAILS("Bluetooth A2DP sink", + "Sink/Audio", + "Plays audio to an A2DP device", + "Marcel Holtmann "); + +static GstStaticPadTemplate gst_a2dp_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "mode = (string) { \"mono\", \"dual\", \"stereo\", \"joint\" }, " + "blocks = (int) { 4, 8, 12, 16 }, " + "subbands = (int) { 4, 8 }, " + "allocation = (string) { \"snr\", \"loudness\" }, " + "bitpool = (int) [ 2, " + TEMPLATE_MAX_BITPOOL_STR " ]; " + "audio/mpeg" + )); + +static gboolean gst_a2dp_sink_handle_event(GstPad *pad, GstEvent *event); +static gboolean gst_a2dp_sink_set_caps(GstPad *pad, GstCaps *caps); +static GstCaps *gst_a2dp_sink_get_caps(GstPad *pad); +static gboolean gst_a2dp_sink_init_caps_filter(GstA2dpSink *self); +static gboolean gst_a2dp_sink_init_fakesink(GstA2dpSink *self); +static gboolean gst_a2dp_sink_remove_fakesink(GstA2dpSink *self); + +static void gst_a2dp_sink_finalize(GObject *obj) +{ + GstA2dpSink *self = GST_A2DP_SINK(obj); + + g_mutex_free(self->cb_mutex); + + G_OBJECT_CLASS(parent_class)->finalize(obj); +} + +static GstState gst_a2dp_sink_get_state(GstA2dpSink *self) +{ + GstState current, pending; + + gst_element_get_state(GST_ELEMENT(self), ¤t, &pending, 0); + if (pending == GST_STATE_VOID_PENDING) + return current; + + return pending; +} + +/* + * Helper function to create elements, add to the bin and link it + * to another element. + */ +static GstElement *gst_a2dp_sink_init_element(GstA2dpSink *self, + const gchar *elementname, const gchar *name, + GstElement *link_to) +{ + GstElement *element; + GstState state; + + GST_LOG_OBJECT(self, "Initializing %s", elementname); + + element = gst_element_factory_make(elementname, name); + if (element == NULL) { + GST_DEBUG_OBJECT(self, "Couldn't create %s", elementname); + return NULL; + } + + if (!gst_bin_add(GST_BIN(self), element)) { + GST_DEBUG_OBJECT(self, "failed to add %s to the bin", + elementname); + goto cleanup_and_fail; + } + + state = gst_a2dp_sink_get_state(self); + if (gst_element_set_state(element, state) == + GST_STATE_CHANGE_FAILURE) { + GST_DEBUG_OBJECT(self, "%s failed to go to playing", + elementname); + goto remove_element_and_fail; + } + + if (link_to != NULL) + if (!gst_element_link(link_to, element)) { + GST_DEBUG_OBJECT(self, "couldn't link %s", + elementname); + goto remove_element_and_fail; + } + + return element; + +remove_element_and_fail: + gst_element_set_state(element, GST_STATE_NULL); + gst_bin_remove(GST_BIN(self), element); + return NULL; + +cleanup_and_fail: + g_object_unref(G_OBJECT(element)); + + return NULL; +} + +static void gst_a2dp_sink_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_set_details(element_class, + &gst_a2dp_sink_details); + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&gst_a2dp_sink_factory)); +} + +static void gst_a2dp_sink_set_property(GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + GstA2dpSink *self = GST_A2DP_SINK(object); + + switch (prop_id) { + case PROP_DEVICE: + if (self->sink != NULL) + gst_avdtp_sink_set_device(self->sink, + g_value_get_string(value)); + + if (self->device != NULL) + g_free(self->device); + self->device = g_value_dup_string(value); + break; + + case PROP_TRANSPORT: + if (self->sink != NULL) + gst_avdtp_sink_set_transport(self->sink, + g_value_get_string(value)); + + if (self->transport != NULL) + g_free(self->transport); + self->transport = g_value_dup_string(value); + break; + + case PROP_AUTOCONNECT: + self->autoconnect = g_value_get_boolean(value); + + if (self->sink != NULL) + g_object_set(G_OBJECT(self->sink), "auto-connect", + self->autoconnect, NULL); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_a2dp_sink_get_property(GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GstA2dpSink *self = GST_A2DP_SINK(object); + gchar *device, *transport; + + switch (prop_id) { + case PROP_DEVICE: + if (self->sink != NULL) { + device = gst_avdtp_sink_get_device(self->sink); + if (device != NULL) + g_value_take_string(value, device); + } + break; + case PROP_AUTOCONNECT: + if (self->sink != NULL) + g_object_get(G_OBJECT(self->sink), "auto-connect", + &self->autoconnect, NULL); + + g_value_set_boolean(value, self->autoconnect); + break; + case PROP_TRANSPORT: + if (self->sink != NULL) { + transport = gst_avdtp_sink_get_transport(self->sink); + if (transport != NULL) + g_value_take_string(value, transport); + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static gboolean gst_a2dp_sink_init_ghost_pad(GstA2dpSink *self) +{ + GstPad *capsfilter_pad; + + /* we search for the capsfilter sinkpad */ + capsfilter_pad = gst_element_get_static_pad(self->capsfilter, "sink"); + + /* now we add a ghostpad */ + self->ghostpad = GST_GHOST_PAD(gst_ghost_pad_new("sink", + capsfilter_pad)); + g_object_unref(capsfilter_pad); + + /* the getcaps of our ghostpad must reflect the device caps */ + gst_pad_set_getcaps_function(GST_PAD(self->ghostpad), + gst_a2dp_sink_get_caps); + self->ghostpad_setcapsfunc = GST_PAD_SETCAPSFUNC(self->ghostpad); + gst_pad_set_setcaps_function(GST_PAD(self->ghostpad), + GST_DEBUG_FUNCPTR(gst_a2dp_sink_set_caps)); + + /* we need to handle events on our own and we also need the eventfunc + * of the ghostpad for forwarding calls */ + self->ghostpad_eventfunc = GST_PAD_EVENTFUNC(GST_PAD(self->ghostpad)); + gst_pad_set_event_function(GST_PAD(self->ghostpad), + gst_a2dp_sink_handle_event); + + if (!gst_element_add_pad(GST_ELEMENT(self), GST_PAD(self->ghostpad))) + GST_ERROR_OBJECT(self, "failed to add ghostpad"); + + return TRUE; +} + +static void gst_a2dp_sink_remove_dynamic_elements(GstA2dpSink *self) +{ + if (self->rtp) { + GST_LOG_OBJECT(self, "removing rtp element from the bin"); + if (!gst_bin_remove(GST_BIN(self), GST_ELEMENT(self->rtp))) + GST_WARNING_OBJECT(self, "failed to remove rtp " + "element from bin"); + else + self->rtp = NULL; + } +} + +static GstStateChangeReturn gst_a2dp_sink_change_state(GstElement *element, + GstStateChange transition) +{ + GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS; + GstA2dpSink *self = GST_A2DP_SINK(element); + + switch (transition) { + case GST_STATE_CHANGE_READY_TO_PAUSED: + self->taglist = gst_tag_list_new(); + + gst_a2dp_sink_init_fakesink(self); + break; + + case GST_STATE_CHANGE_NULL_TO_READY: + self->sink_is_in_bin = FALSE; + self->sink = GST_AVDTP_SINK(gst_element_factory_make( + "avdtpsink", "avdtpsink")); + if (self->sink == NULL) { + GST_WARNING_OBJECT(self, "failed to create avdtpsink"); + return GST_STATE_CHANGE_FAILURE; + } + + if (self->device != NULL) + gst_avdtp_sink_set_device(self->sink, + self->device); + + if (self->transport != NULL) + gst_avdtp_sink_set_transport(self->sink, + self->transport); + + g_object_set(G_OBJECT(self->sink), "auto-connect", + self->autoconnect, NULL); + + ret = gst_element_set_state(GST_ELEMENT(self->sink), + GST_STATE_READY); + break; + default: + break; + } + + if (ret == GST_STATE_CHANGE_FAILURE) + return ret; + + ret = GST_ELEMENT_CLASS(parent_class)->change_state(element, + transition); + + switch (transition) { + case GST_STATE_CHANGE_PAUSED_TO_READY: + if (self->taglist) { + gst_tag_list_free(self->taglist); + self->taglist = NULL; + } + if (self->newseg_event != NULL) { + gst_event_unref(self->newseg_event); + self->newseg_event = NULL; + } + gst_a2dp_sink_remove_fakesink(self); + break; + + case GST_STATE_CHANGE_READY_TO_NULL: + if (self->sink_is_in_bin) { + if (!gst_bin_remove(GST_BIN(self), + GST_ELEMENT(self->sink))) + GST_WARNING_OBJECT(self, "Failed to remove " + "avdtpsink from bin"); + } else if (self->sink != NULL) { + gst_element_set_state(GST_ELEMENT(self->sink), + GST_STATE_NULL); + g_object_unref(G_OBJECT(self->sink)); + } + + self->sink = NULL; + + gst_a2dp_sink_remove_dynamic_elements(self); + break; + default: + break; + } + + return ret; +} + +static void gst_a2dp_sink_class_init(GstA2dpSinkClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GstElementClass *element_class = GST_ELEMENT_CLASS(klass); + + parent_class = g_type_class_peek_parent(klass); + + object_class->set_property = GST_DEBUG_FUNCPTR( + gst_a2dp_sink_set_property); + object_class->get_property = GST_DEBUG_FUNCPTR( + gst_a2dp_sink_get_property); + + object_class->finalize = GST_DEBUG_FUNCPTR( + gst_a2dp_sink_finalize); + + element_class->change_state = GST_DEBUG_FUNCPTR( + gst_a2dp_sink_change_state); + + g_object_class_install_property(object_class, PROP_DEVICE, + g_param_spec_string("device", "Device", + "Bluetooth remote device address", + NULL, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_AUTOCONNECT, + g_param_spec_boolean("auto-connect", "Auto-connect", + "Automatically attempt to connect to device", + DEFAULT_AUTOCONNECT, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_TRANSPORT, + g_param_spec_string("transport", "Transport", + "Use configured transport", + NULL, G_PARAM_READWRITE)); + + GST_DEBUG_CATEGORY_INIT(gst_a2dp_sink_debug, "a2dpsink", 0, + "A2DP sink element"); +} + +GstCaps *gst_a2dp_sink_get_device_caps(GstA2dpSink *self) +{ + return gst_avdtp_sink_get_device_caps(self->sink); +} + +static GstCaps *gst_a2dp_sink_get_caps(GstPad *pad) +{ + GstCaps *caps; + GstCaps *caps_aux; + GstA2dpSink *self = GST_A2DP_SINK(GST_PAD_PARENT(pad)); + + if (self->sink == NULL) { + GST_DEBUG_OBJECT(self, "a2dpsink isn't initialized " + "returning template caps"); + caps = gst_static_pad_template_get_caps( + &gst_a2dp_sink_factory); + } else { + GST_LOG_OBJECT(self, "Getting device caps"); + caps = gst_a2dp_sink_get_device_caps(self); + if (caps == NULL) + caps = gst_static_pad_template_get_caps( + &gst_a2dp_sink_factory); + } + caps_aux = gst_caps_copy(caps); + g_object_set(self->capsfilter, "caps", caps_aux, NULL); + gst_caps_unref(caps_aux); + return caps; +} + +static gboolean gst_a2dp_sink_init_avdtp_sink(GstA2dpSink *self) +{ + GstElement *sink; + + /* check if we don't need a new sink */ + if (self->sink_is_in_bin) + return TRUE; + + if (self->sink == NULL) + sink = gst_element_factory_make("avdtpsink", "avdtpsink"); + else + sink = GST_ELEMENT(self->sink); + + if (sink == NULL) { + GST_ERROR_OBJECT(self, "Couldn't create avdtpsink"); + return FALSE; + } + + if (!gst_bin_add(GST_BIN(self), sink)) { + GST_ERROR_OBJECT(self, "failed to add avdtpsink " + "to the bin"); + goto cleanup_and_fail; + } + + if (gst_element_set_state(sink, GST_STATE_READY) == + GST_STATE_CHANGE_FAILURE) { + GST_ERROR_OBJECT(self, "avdtpsink failed to go to ready"); + goto remove_element_and_fail; + } + + if (!gst_element_link(GST_ELEMENT(self->rtp), sink)) { + GST_ERROR_OBJECT(self, "couldn't link rtpsbcpay " + "to avdtpsink"); + goto remove_element_and_fail; + } + + self->sink = GST_AVDTP_SINK(sink); + self->sink_is_in_bin = TRUE; + g_object_set(G_OBJECT(self->sink), "device", self->device, NULL); + g_object_set(G_OBJECT(self->sink), "transport", self->transport, NULL); + + gst_element_set_state(sink, GST_STATE_PAUSED); + + return TRUE; + +remove_element_and_fail: + gst_element_set_state(sink, GST_STATE_NULL); + gst_bin_remove(GST_BIN(self), sink); + return FALSE; + +cleanup_and_fail: + if (sink != NULL) + g_object_unref(G_OBJECT(sink)); + + return FALSE; +} + +static gboolean gst_a2dp_sink_init_rtp_sbc_element(GstA2dpSink *self) +{ + GstElement *rtppay; + + /* if we already have a rtp, we don't need a new one */ + if (self->rtp != NULL) + return TRUE; + + rtppay = gst_a2dp_sink_init_element(self, "rtpsbcpay", "rtp", + self->capsfilter); + if (rtppay == NULL) + return FALSE; + + self->rtp = GST_BASE_RTP_PAYLOAD(rtppay); + g_object_set(G_OBJECT(self->rtp), "min-frames", -1, NULL); + + gst_element_set_state(rtppay, GST_STATE_PAUSED); + + return TRUE; +} + +static gboolean gst_a2dp_sink_init_rtp_mpeg_element(GstA2dpSink *self) +{ + GstElement *rtppay; + + /* check if we don't need a new rtp */ + if (self->rtp) + return TRUE; + + GST_LOG_OBJECT(self, "Initializing rtp mpeg element"); + /* if capsfilter is not created then we can't have our rtp element */ + if (self->capsfilter == NULL) + return FALSE; + + rtppay = gst_a2dp_sink_init_element(self, "rtpmpapay", "rtp", + self->capsfilter); + if (rtppay == NULL) + return FALSE; + + self->rtp = GST_BASE_RTP_PAYLOAD(rtppay); + + gst_element_set_state(rtppay, GST_STATE_PAUSED); + + return TRUE; +} + +static gboolean gst_a2dp_sink_init_dynamic_elements(GstA2dpSink *self, + GstCaps *caps) +{ + GstStructure *structure; + GstEvent *event; + GstPad *capsfilterpad; + gboolean crc; + gchar *mode = NULL; + + structure = gst_caps_get_structure(caps, 0); + + /* before everything we need to remove fakesink */ + gst_a2dp_sink_remove_fakesink(self); + + /* first, we need to create our rtp payloader */ + if (gst_structure_has_name(structure, "audio/x-sbc")) { + GST_LOG_OBJECT(self, "sbc media received"); + if (!gst_a2dp_sink_init_rtp_sbc_element(self)) + return FALSE; + } else if (gst_structure_has_name(structure, "audio/mpeg")) { + GST_LOG_OBJECT(self, "mp3 media received"); + if (!gst_a2dp_sink_init_rtp_mpeg_element(self)) + return FALSE; + } else { + GST_ERROR_OBJECT(self, "Unexpected media type"); + return FALSE; + } + + if (!gst_a2dp_sink_init_avdtp_sink(self)) + return FALSE; + + /* check if we should push the taglist FIXME should we push this? + * we can send the tags directly if needed */ + if (self->taglist != NULL && + gst_structure_has_name(structure, "audio/mpeg")) { + + event = gst_event_new_tag(self->taglist); + + /* send directly the crc */ + if (gst_tag_list_get_boolean(self->taglist, "has-crc", &crc)) + gst_avdtp_sink_set_crc(self->sink, crc); + + if (gst_tag_list_get_string(self->taglist, "channel-mode", + &mode)) + gst_avdtp_sink_set_channel_mode(self->sink, mode); + + capsfilterpad = gst_ghost_pad_get_target(self->ghostpad); + gst_pad_send_event(capsfilterpad, event); + self->taglist = NULL; + g_free(mode); + } + + if (!gst_avdtp_sink_set_device_caps(self->sink, caps)) + return FALSE; + + g_object_set(G_OBJECT(self->rtp), "mtu", + gst_avdtp_sink_get_link_mtu(self->sink), NULL); + + /* we forward our new segment here if we have one */ + if (self->newseg_event) { + gst_pad_send_event(GST_BASE_RTP_PAYLOAD_SINKPAD(self->rtp), + self->newseg_event); + self->newseg_event = NULL; + } + + return TRUE; +} + +static gboolean gst_a2dp_sink_set_caps(GstPad *pad, GstCaps *caps) +{ + GstA2dpSink *self; + + self = GST_A2DP_SINK(GST_PAD_PARENT(pad)); + GST_INFO_OBJECT(self, "setting caps"); + + /* now we know the caps */ + gst_a2dp_sink_init_dynamic_elements(self, caps); + + return self->ghostpad_setcapsfunc(GST_PAD(self->ghostpad), caps); +} + +/* used for catching newsegment events while we don't have a sink, for + * later forwarding it to the sink */ +static gboolean gst_a2dp_sink_handle_event(GstPad *pad, GstEvent *event) +{ + GstA2dpSink *self; + GstTagList *taglist = NULL; + GstObject *parent; + + self = GST_A2DP_SINK(GST_PAD_PARENT(pad)); + parent = gst_element_get_parent(GST_ELEMENT(self->sink)); + + if (GST_EVENT_TYPE(event) == GST_EVENT_NEWSEGMENT && + parent != GST_OBJECT_CAST(self)) { + if (self->newseg_event != NULL) + gst_event_unref(self->newseg_event); + self->newseg_event = gst_event_ref(event); + + } else if (GST_EVENT_TYPE(event) == GST_EVENT_TAG && + parent != GST_OBJECT_CAST(self)) { + if (self->taglist == NULL) + gst_event_parse_tag(event, &self->taglist); + else { + gst_event_parse_tag(event, &taglist); + gst_tag_list_insert(self->taglist, taglist, + GST_TAG_MERGE_REPLACE); + } + } + + if (parent != NULL) + gst_object_unref(GST_OBJECT(parent)); + + return self->ghostpad_eventfunc(GST_PAD(self->ghostpad), event); +} + +static gboolean gst_a2dp_sink_init_caps_filter(GstA2dpSink *self) +{ + GstElement *element; + + element = gst_element_factory_make("capsfilter", "filter"); + if (element == NULL) + goto failed; + + if (!gst_bin_add(GST_BIN(self), element)) + goto failed; + + self->capsfilter = element; + return TRUE; + +failed: + GST_ERROR_OBJECT(self, "Failed to initialize caps filter"); + return FALSE; +} + +static gboolean gst_a2dp_sink_init_fakesink(GstA2dpSink *self) +{ + if (self->fakesink != NULL) + return TRUE; + + g_mutex_lock(self->cb_mutex); + self->fakesink = gst_a2dp_sink_init_element(self, "fakesink", + "fakesink", self->capsfilter); + g_mutex_unlock(self->cb_mutex); + + if (!self->fakesink) + return FALSE; + + return TRUE; +} + +static gboolean gst_a2dp_sink_remove_fakesink(GstA2dpSink *self) +{ + g_mutex_lock(self->cb_mutex); + + if (self->fakesink != NULL) { + gst_element_set_locked_state(self->fakesink, TRUE); + gst_element_set_state(self->fakesink, GST_STATE_NULL); + + gst_bin_remove(GST_BIN(self), self->fakesink); + self->fakesink = NULL; + } + + g_mutex_unlock(self->cb_mutex); + + return TRUE; +} + +static void gst_a2dp_sink_init(GstA2dpSink *self, + GstA2dpSinkClass *klass) +{ + self->sink = NULL; + self->fakesink = NULL; + self->rtp = NULL; + self->device = NULL; + self->transport = NULL; + self->autoconnect = DEFAULT_AUTOCONNECT; + self->capsfilter = NULL; + self->newseg_event = NULL; + self->taglist = NULL; + self->ghostpad = NULL; + self->sink_is_in_bin = FALSE; + + self->cb_mutex = g_mutex_new(); + + /* we initialize our capsfilter */ + gst_a2dp_sink_init_caps_filter(self); + g_object_set(self->capsfilter, "caps", + gst_static_pad_template_get_caps(&gst_a2dp_sink_factory), + NULL); + + gst_a2dp_sink_init_fakesink(self); + + gst_a2dp_sink_init_ghost_pad(self); + +} + +gboolean gst_a2dp_sink_plugin_init(GstPlugin *plugin) +{ + return gst_element_register(plugin, "a2dpsink", + GST_RANK_MARGINAL, GST_TYPE_A2DP_SINK); +} diff --git a/audio/gsta2dpsink.h b/audio/gsta2dpsink.h new file mode 100644 index 0000000..1a591b2 --- /dev/null +++ b/audio/gsta2dpsink.h @@ -0,0 +1,84 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __GST_A2DP_SINK_H__ +#define __GST_A2DP_SINK_H__ + +#include +#include +#include "gstavdtpsink.h" + +G_BEGIN_DECLS + +#define GST_TYPE_A2DP_SINK \ + (gst_a2dp_sink_get_type()) +#define GST_A2DP_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_A2DP_SINK,GstA2dpSink)) +#define GST_A2DP_SINK_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_A2DP_SINK,GstA2dpSinkClass)) +#define GST_IS_A2DP_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_A2DP_SINK)) +#define GST_IS_A2DP_SINK_CLASS(obj) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_A2DP_SINK)) + +typedef struct _GstA2dpSink GstA2dpSink; +typedef struct _GstA2dpSinkClass GstA2dpSinkClass; + +struct _GstA2dpSink { + GstBin bin; + + GstBaseRTPPayload *rtp; + GstAvdtpSink *sink; + GstElement *capsfilter; + GstElement *fakesink; + + gchar *device; + gchar *transport; + gboolean autoconnect; + gboolean sink_is_in_bin; + + GstGhostPad *ghostpad; + GstPadSetCapsFunction ghostpad_setcapsfunc; + GstPadEventFunction ghostpad_eventfunc; + + GstEvent *newseg_event; + /* Store the tags received before the a2dpsender sink is created + * when it is created we forward this to it */ + GstTagList *taglist; + + GMutex *cb_mutex; +}; + +struct _GstA2dpSinkClass { + GstBinClass parent_class; +}; + +GType gst_a2dp_sink_get_type(void); + +gboolean gst_a2dp_sink_plugin_init (GstPlugin * plugin); + +GstCaps *gst_a2dp_sink_get_device_caps(GstA2dpSink *self); + +G_END_DECLS + +#endif diff --git a/audio/gstavdtpsink.c b/audio/gstavdtpsink.c new file mode 100644 index 0000000..1f374fc --- /dev/null +++ b/audio/gstavdtpsink.c @@ -0,0 +1,2034 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include + +#include "ipc.h" +#include "rtp.h" +#include "a2dp-codecs.h" + +#include "gstpragma.h" +#include "gstavdtpsink.h" + +GST_DEBUG_CATEGORY_STATIC(avdtp_sink_debug); +#define GST_CAT_DEFAULT avdtp_sink_debug + +#define BUFFER_SIZE 2048 +#define TEMPLATE_MAX_BITPOOL 64 +#define CRC_PROTECTED 1 +#define CRC_UNPROTECTED 0 + +#define DEFAULT_AUTOCONNECT TRUE + +#define GST_AVDTP_SINK_MUTEX_LOCK(s) G_STMT_START { \ + g_mutex_lock(s->sink_lock); \ + } G_STMT_END + +#define GST_AVDTP_SINK_MUTEX_UNLOCK(s) G_STMT_START { \ + g_mutex_unlock(s->sink_lock); \ + } G_STMT_END + +struct bluetooth_data { + struct bt_get_capabilities_rsp *caps; /* Bluetooth device caps */ + guint link_mtu; + + DBusConnection *conn; + guint8 codec; /* Bluetooth transport configuration */ + gchar *uuid; + guint8 *config; + gint config_size; + + gchar buffer[BUFFER_SIZE]; /* Codec transfer buffer */ +}; + +#define IS_SBC(n) (strcmp((n), "audio/x-sbc") == 0) +#define IS_MPEG_AUDIO(n) (strcmp((n), "audio/mpeg") == 0) + +enum { + PROP_0, + PROP_DEVICE, + PROP_AUTOCONNECT, + PROP_TRANSPORT +}; + +GST_BOILERPLATE(GstAvdtpSink, gst_avdtp_sink, GstBaseSink, + GST_TYPE_BASE_SINK); + +static const GstElementDetails avdtp_sink_details = + GST_ELEMENT_DETAILS("Bluetooth AVDTP sink", + "Sink/Audio", + "Plays audio to an A2DP device", + "Marcel Holtmann "); + +static GstStaticPadTemplate avdtp_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("application/x-rtp, " + "media = (string) \"audio\"," + "payload = (int) " + GST_RTP_PAYLOAD_DYNAMIC_STRING ", " + "clock-rate = (int) { 16000, 32000, " + "44100, 48000 }, " + "encoding-name = (string) \"SBC\"; " + "application/x-rtp, " + "media = (string) \"audio\", " + "payload = (int) " + GST_RTP_PAYLOAD_MPA_STRING ", " + "clock-rate = (int) 90000; " + "application/x-rtp, " + "media = (string) \"audio\", " + "payload = (int) " + GST_RTP_PAYLOAD_DYNAMIC_STRING ", " + "clock-rate = (int) 90000, " + "encoding-name = (string) \"MPA\"" + )); + +static int gst_avdtp_sink_audioservice_send(GstAvdtpSink *self, + const bt_audio_msg_header_t *msg); +static int gst_avdtp_sink_audioservice_expect(GstAvdtpSink *self, + bt_audio_msg_header_t *outmsg, + guint8 expected_name); + + +static void gst_avdtp_sink_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&avdtp_sink_factory)); + + gst_element_class_set_details(element_class, &avdtp_sink_details); +} + +static void gst_avdtp_sink_transport_release(GstAvdtpSink *self) +{ + DBusMessage *msg; + const char *access_type = "w"; + + msg = dbus_message_new_method_call("org.bluez", self->transport, + "org.bluez.MediaTransport", + "Release"); + + dbus_message_append_args(msg, DBUS_TYPE_STRING, &access_type, + DBUS_TYPE_INVALID); + + dbus_connection_send(self->data->conn, msg, NULL); + + dbus_message_unref(msg); +} + +static gboolean gst_avdtp_sink_stop(GstBaseSink *basesink) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(basesink); + + GST_INFO_OBJECT(self, "stop"); + + if (self->watch_id != 0) { + g_source_remove(self->watch_id); + self->watch_id = 0; + } + + if (self->server) { + bt_audio_service_close(g_io_channel_unix_get_fd(self->server)); + g_io_channel_unref(self->server); + self->server = NULL; + } + + if (self->stream) { + g_io_channel_shutdown(self->stream, TRUE, NULL); + g_io_channel_unref(self->stream); + self->stream = NULL; + } + + if (self->data) { + if (self->transport) + gst_avdtp_sink_transport_release(self); + if (self->data->conn) + dbus_connection_unref(self->data->conn); + g_free(self->data); + self->data = NULL; + } + + if (self->stream_caps) { + gst_caps_unref(self->stream_caps); + self->stream_caps = NULL; + } + + if (self->dev_caps) { + gst_caps_unref(self->dev_caps); + self->dev_caps = NULL; + } + + return TRUE; +} + +static void gst_avdtp_sink_finalize(GObject *object) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(object); + + if (self->data) + gst_avdtp_sink_stop(GST_BASE_SINK(self)); + + if (self->device) + g_free(self->device); + + if (self->transport) + g_free(self->transport); + + g_mutex_free(self->sink_lock); + + G_OBJECT_CLASS(parent_class)->finalize(object); +} + +static void gst_avdtp_sink_set_property(GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + GstAvdtpSink *sink = GST_AVDTP_SINK(object); + + switch (prop_id) { + case PROP_DEVICE: + if (sink->device) + g_free(sink->device); + sink->device = g_value_dup_string(value); + break; + + case PROP_AUTOCONNECT: + sink->autoconnect = g_value_get_boolean(value); + break; + + case PROP_TRANSPORT: + if (sink->transport) + g_free(sink->transport); + sink->transport = g_value_dup_string(value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_avdtp_sink_get_property(GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GstAvdtpSink *sink = GST_AVDTP_SINK(object); + + switch (prop_id) { + case PROP_DEVICE: + g_value_set_string(value, sink->device); + break; + + case PROP_AUTOCONNECT: + g_value_set_boolean(value, sink->autoconnect); + break; + + case PROP_TRANSPORT: + g_value_set_string(value, sink->transport); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static gint gst_avdtp_sink_bluetooth_recvmsg_fd(GstAvdtpSink *sink) +{ + int err, ret; + + ret = bt_audio_service_get_data_fd( + g_io_channel_unix_get_fd(sink->server)); + + if (ret < 0) { + err = -errno; + GST_ERROR_OBJECT(sink, "Unable to receive fd: %s (%d)", + strerror(-err), -err); + return err; + } + + sink->stream = g_io_channel_unix_new(ret); + g_io_channel_set_encoding(sink->stream, NULL, NULL); + GST_DEBUG_OBJECT(sink, "stream_fd=%d", ret); + + return 0; +} + +static codec_capabilities_t *gst_avdtp_find_caps(GstAvdtpSink *sink, + uint8_t codec_type) +{ + struct bt_get_capabilities_rsp *rsp = sink->data->caps; + codec_capabilities_t *codec = (void *) rsp->data; + int bytes_left = rsp->h.length - sizeof(*rsp); + + while (bytes_left > 0) { + if ((codec->type == codec_type) && + !(codec->lock & BT_WRITE_LOCK)) + break; + + bytes_left -= codec->length; + codec = (void *) codec + codec->length; + } + + if (bytes_left <= 0) + return NULL; + + return codec; +} + +static gboolean gst_avdtp_sink_init_sbc_pkt_conf(GstAvdtpSink *sink, + GstCaps *caps, + sbc_capabilities_t *pkt) +{ + sbc_capabilities_t *cfg; + const GValue *value = NULL; + const char *pref, *name; + gint rate, subbands, blocks; + GstStructure *structure = gst_caps_get_structure(caps, 0); + + cfg = (void *) gst_avdtp_find_caps(sink, BT_A2DP_SBC_SINK); + name = gst_structure_get_name(structure); + + if (!(IS_SBC(name))) { + GST_ERROR_OBJECT(sink, "Unexpected format %s, " + "was expecting sbc", name); + return FALSE; + } + + value = gst_structure_get_value(structure, "rate"); + rate = g_value_get_int(value); + if (rate == 44100) + cfg->frequency = BT_SBC_SAMPLING_FREQ_44100; + else if (rate == 48000) + cfg->frequency = BT_SBC_SAMPLING_FREQ_48000; + else if (rate == 32000) + cfg->frequency = BT_SBC_SAMPLING_FREQ_32000; + else if (rate == 16000) + cfg->frequency = BT_SBC_SAMPLING_FREQ_16000; + else { + GST_ERROR_OBJECT(sink, "Invalid rate while setting caps"); + return FALSE; + } + + value = gst_structure_get_value(structure, "mode"); + pref = g_value_get_string(value); + if (strcmp(pref, "mono") == 0) + cfg->channel_mode = BT_A2DP_CHANNEL_MODE_MONO; + else if (strcmp(pref, "dual") == 0) + cfg->channel_mode = BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL; + else if (strcmp(pref, "stereo") == 0) + cfg->channel_mode = BT_A2DP_CHANNEL_MODE_STEREO; + else if (strcmp(pref, "joint") == 0) + cfg->channel_mode = BT_A2DP_CHANNEL_MODE_JOINT_STEREO; + else { + GST_ERROR_OBJECT(sink, "Invalid mode %s", pref); + return FALSE; + } + + value = gst_structure_get_value(structure, "allocation"); + pref = g_value_get_string(value); + if (strcmp(pref, "loudness") == 0) + cfg->allocation_method = BT_A2DP_ALLOCATION_LOUDNESS; + else if (strcmp(pref, "snr") == 0) + cfg->allocation_method = BT_A2DP_ALLOCATION_SNR; + else { + GST_ERROR_OBJECT(sink, "Invalid allocation: %s", pref); + return FALSE; + } + + value = gst_structure_get_value(structure, "subbands"); + subbands = g_value_get_int(value); + if (subbands == 8) + cfg->subbands = BT_A2DP_SUBBANDS_8; + else if (subbands == 4) + cfg->subbands = BT_A2DP_SUBBANDS_4; + else { + GST_ERROR_OBJECT(sink, "Invalid subbands %d", subbands); + return FALSE; + } + + value = gst_structure_get_value(structure, "blocks"); + blocks = g_value_get_int(value); + if (blocks == 16) + cfg->block_length = BT_A2DP_BLOCK_LENGTH_16; + else if (blocks == 12) + cfg->block_length = BT_A2DP_BLOCK_LENGTH_12; + else if (blocks == 8) + cfg->block_length = BT_A2DP_BLOCK_LENGTH_8; + else if (blocks == 4) + cfg->block_length = BT_A2DP_BLOCK_LENGTH_4; + else { + GST_ERROR_OBJECT(sink, "Invalid blocks %d", blocks); + return FALSE; + } + + value = gst_structure_get_value(structure, "bitpool"); + cfg->max_bitpool = cfg->min_bitpool = g_value_get_int(value); + + memcpy(pkt, cfg, sizeof(*pkt)); + + return TRUE; +} + +static gboolean gst_avdtp_sink_conf_recv_stream_fd( + GstAvdtpSink *self) +{ + struct bluetooth_data *data = self->data; + gint ret; + GError *gerr = NULL; + GIOStatus status; + GIOFlags flags; + int fd; + + /* Proceed if stream was already acquired */ + if (self->stream != NULL) + goto proceed; + + ret = gst_avdtp_sink_bluetooth_recvmsg_fd(self); + if (ret < 0) + return FALSE; + + if (!self->stream) { + GST_ERROR_OBJECT(self, "Error while configuring device: " + "could not acquire audio socket"); + return FALSE; + } + +proceed: + /* set stream socket to nonblock */ + GST_LOG_OBJECT(self, "setting stream socket to nonblock"); + flags = g_io_channel_get_flags(self->stream); + flags |= G_IO_FLAG_NONBLOCK; + status = g_io_channel_set_flags(self->stream, flags, &gerr); + if (status != G_IO_STATUS_NORMAL) { + if (gerr) + GST_WARNING_OBJECT(self, "Error while " + "setting server socket to nonblock: " + "%s", gerr->message); + else + GST_WARNING_OBJECT(self, "Error while " + "setting server " + "socket to nonblock"); + } + + fd = g_io_channel_unix_get_fd(self->stream); + + /* It is possible there is some outstanding + data in the pipe - we have to empty it */ + GST_LOG_OBJECT(self, "emptying stream pipe"); + while (1) { + ssize_t bread = read(fd, data->buffer, data->link_mtu); + if (bread <= 0) + break; + } + + /* set stream socket to block */ + GST_LOG_OBJECT(self, "setting stream socket to block"); + flags = g_io_channel_get_flags(self->stream); + flags &= ~G_IO_FLAG_NONBLOCK; + status = g_io_channel_set_flags(self->stream, flags, &gerr); + if (status != G_IO_STATUS_NORMAL) { + if (gerr) + GST_WARNING_OBJECT(self, "Error while " + "setting server socket to block:" + "%s", gerr->message); + else + GST_WARNING_OBJECT(self, "Error while " + "setting server " + "socket to block"); + } + + memset(data->buffer, 0, sizeof(data->buffer)); + + return TRUE; +} + +static gboolean server_callback(GIOChannel *chan, + GIOCondition cond, gpointer data) +{ + if (cond & G_IO_HUP || cond & G_IO_NVAL) + return FALSE; + else if (cond & G_IO_ERR) + GST_WARNING_OBJECT(GST_AVDTP_SINK(data), + "Untreated callback G_IO_ERR"); + + return TRUE; +} + +static GstStructure *gst_avdtp_sink_parse_sbc_caps( + GstAvdtpSink *self, sbc_capabilities_t *sbc) +{ + GstStructure *structure; + GValue *value; + GValue *list; + gboolean mono, stereo; + + if (sbc == NULL) + return NULL; + + structure = gst_structure_empty_new("audio/x-sbc"); + value = g_value_init(g_new0(GValue, 1), G_TYPE_STRING); + + /* mode */ + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) { + g_value_set_static_string(value, "mono"); + gst_value_list_prepend_value(list, value); + } + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) { + g_value_set_static_string(value, "stereo"); + gst_value_list_prepend_value(list, value); + } + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) { + g_value_set_static_string(value, "dual"); + gst_value_list_prepend_value(list, value); + } + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_JOINT_STEREO) { + g_value_set_static_string(value, "joint"); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "mode", list); + g_free(list); + list = NULL; + } + + /* subbands */ + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + value = g_value_init(value, G_TYPE_INT); + if (sbc->subbands & BT_A2DP_SUBBANDS_4) { + g_value_set_int(value, 4); + gst_value_list_prepend_value(list, value); + } + if (sbc->subbands & BT_A2DP_SUBBANDS_8) { + g_value_set_int(value, 8); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "subbands", list); + g_free(list); + list = NULL; + } + + /* blocks */ + value = g_value_init(value, G_TYPE_INT); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_16) { + g_value_set_int(value, 16); + gst_value_list_prepend_value(list, value); + } + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_12) { + g_value_set_int(value, 12); + gst_value_list_prepend_value(list, value); + } + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_8) { + g_value_set_int(value, 8); + gst_value_list_prepend_value(list, value); + } + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_4) { + g_value_set_int(value, 4); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "blocks", list); + g_free(list); + list = NULL; + } + + /* allocation */ + g_value_init(value, G_TYPE_STRING); + list = g_value_init(g_new0(GValue,1), GST_TYPE_LIST); + if (sbc->allocation_method & BT_A2DP_ALLOCATION_LOUDNESS) { + g_value_set_static_string(value, "loudness"); + gst_value_list_prepend_value(list, value); + } + if (sbc->allocation_method & BT_A2DP_ALLOCATION_SNR) { + g_value_set_static_string(value, "snr"); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "allocation", list); + g_free(list); + list = NULL; + } + + /* rate */ + g_value_init(value, G_TYPE_INT); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_48000) { + g_value_set_int(value, 48000); + gst_value_list_prepend_value(list, value); + } + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_44100) { + g_value_set_int(value, 44100); + gst_value_list_prepend_value(list, value); + } + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_32000) { + g_value_set_int(value, 32000); + gst_value_list_prepend_value(list, value); + } + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_16000) { + g_value_set_int(value, 16000); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "rate", list); + g_free(list); + list = NULL; + } + + /* bitpool */ + value = g_value_init(value, GST_TYPE_INT_RANGE); + gst_value_set_int_range(value, + MIN(sbc->min_bitpool, TEMPLATE_MAX_BITPOOL), + MIN(sbc->max_bitpool, TEMPLATE_MAX_BITPOOL)); + gst_structure_set_value(structure, "bitpool", value); + g_value_unset(value); + + /* channels */ + mono = FALSE; + stereo = FALSE; + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) + mono = TRUE; + if ((sbc->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) || + (sbc->channel_mode & + BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) || + (sbc->channel_mode & + BT_A2DP_CHANNEL_MODE_JOINT_STEREO)) + stereo = TRUE; + + if (mono && stereo) { + g_value_init(value, GST_TYPE_INT_RANGE); + gst_value_set_int_range(value, 1, 2); + } else { + g_value_init(value, G_TYPE_INT); + if (mono) + g_value_set_int(value, 1); + else if (stereo) + g_value_set_int(value, 2); + else { + GST_ERROR_OBJECT(self, + "Unexpected number of channels"); + g_value_set_int(value, 0); + } + } + + gst_structure_set_value(structure, "channels", value); + g_free(value); + + return structure; +} + +static GstStructure *gst_avdtp_sink_parse_mpeg_caps( + GstAvdtpSink *self, mpeg_capabilities_t *mpeg) +{ + GstStructure *structure; + GValue *value; + GValue *list; + gboolean valid_layer = FALSE; + gboolean mono, stereo; + + if (!mpeg) + return NULL; + + GST_LOG_OBJECT(self, "parsing mpeg caps"); + + structure = gst_structure_empty_new("audio/mpeg"); + value = g_new0(GValue, 1); + g_value_init(value, G_TYPE_INT); + + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + g_value_set_int(value, 1); + gst_value_list_prepend_value(list, value); + g_value_set_int(value, 2); + gst_value_list_prepend_value(list, value); + gst_structure_set_value(structure, "mpegversion", list); + g_free(list); + + /* layer */ + GST_LOG_OBJECT(self, "setting mpeg layer"); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (mpeg->layer & BT_MPEG_LAYER_1) { + g_value_set_int(value, 1); + gst_value_list_prepend_value(list, value); + valid_layer = TRUE; + } + if (mpeg->layer & BT_MPEG_LAYER_2) { + g_value_set_int(value, 2); + gst_value_list_prepend_value(list, value); + valid_layer = TRUE; + } + if (mpeg->layer & BT_MPEG_LAYER_3) { + g_value_set_int(value, 3); + gst_value_list_prepend_value(list, value); + valid_layer = TRUE; + } + if (list) { + gst_structure_set_value(structure, "layer", list); + g_free(list); + list = NULL; + } + + if (!valid_layer) { + gst_structure_free(structure); + g_free(value); + return NULL; + } + + /* rate */ + GST_LOG_OBJECT(self, "setting mpeg rate"); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_48000) { + g_value_set_int(value, 48000); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_44100) { + g_value_set_int(value, 44100); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_32000) { + g_value_set_int(value, 32000); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_24000) { + g_value_set_int(value, 24000); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_22050) { + g_value_set_int(value, 22050); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_16000) { + g_value_set_int(value, 16000); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "rate", list); + g_free(list); + list = NULL; + } + + /* channels */ + GST_LOG_OBJECT(self, "setting mpeg channels"); + mono = FALSE; + stereo = FALSE; + if (mpeg->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) + mono = TRUE; + if ((mpeg->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) || + (mpeg->channel_mode & + BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) || + (mpeg->channel_mode & + BT_A2DP_CHANNEL_MODE_JOINT_STEREO)) + stereo = TRUE; + + if (mono && stereo) { + g_value_init(value, GST_TYPE_INT_RANGE); + gst_value_set_int_range(value, 1, 2); + } else { + g_value_init(value, G_TYPE_INT); + if (mono) + g_value_set_int(value, 1); + else if (stereo) + g_value_set_int(value, 2); + else { + GST_ERROR_OBJECT(self, + "Unexpected number of channels"); + g_value_set_int(value, 0); + } + } + gst_structure_set_value(structure, "channels", value); + g_free(value); + + return structure; +} + +static GstStructure *gst_avdtp_sink_parse_sbc_raw(GstAvdtpSink *self) +{ + a2dp_sbc_t *sbc = (a2dp_sbc_t *) self->data->config; + GstStructure *structure; + GValue *value; + GValue *list; + gboolean mono, stereo; + + structure = gst_structure_empty_new("audio/x-sbc"); + value = g_value_init(g_new0(GValue, 1), G_TYPE_STRING); + + /* mode */ + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) { + g_value_set_static_string(value, "mono"); + gst_value_list_prepend_value(list, value); + } + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) { + g_value_set_static_string(value, "stereo"); + gst_value_list_prepend_value(list, value); + } + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) { + g_value_set_static_string(value, "dual"); + gst_value_list_prepend_value(list, value); + } + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_JOINT_STEREO) { + g_value_set_static_string(value, "joint"); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "mode", list); + g_free(list); + list = NULL; + } + + /* subbands */ + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + value = g_value_init(value, G_TYPE_INT); + if (sbc->subbands & BT_A2DP_SUBBANDS_4) { + g_value_set_int(value, 4); + gst_value_list_prepend_value(list, value); + } + if (sbc->subbands & BT_A2DP_SUBBANDS_8) { + g_value_set_int(value, 8); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "subbands", list); + g_free(list); + list = NULL; + } + + /* blocks */ + value = g_value_init(value, G_TYPE_INT); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_16) { + g_value_set_int(value, 16); + gst_value_list_prepend_value(list, value); + } + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_12) { + g_value_set_int(value, 12); + gst_value_list_prepend_value(list, value); + } + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_8) { + g_value_set_int(value, 8); + gst_value_list_prepend_value(list, value); + } + if (sbc->block_length & BT_A2DP_BLOCK_LENGTH_4) { + g_value_set_int(value, 4); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "blocks", list); + g_free(list); + list = NULL; + } + + /* allocation */ + g_value_init(value, G_TYPE_STRING); + list = g_value_init(g_new0(GValue,1), GST_TYPE_LIST); + if (sbc->allocation_method & BT_A2DP_ALLOCATION_LOUDNESS) { + g_value_set_static_string(value, "loudness"); + gst_value_list_prepend_value(list, value); + } + if (sbc->allocation_method & BT_A2DP_ALLOCATION_SNR) { + g_value_set_static_string(value, "snr"); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "allocation", list); + g_free(list); + list = NULL; + } + + /* rate */ + g_value_init(value, G_TYPE_INT); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_48000) { + g_value_set_int(value, 48000); + gst_value_list_prepend_value(list, value); + } + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_44100) { + g_value_set_int(value, 44100); + gst_value_list_prepend_value(list, value); + } + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_32000) { + g_value_set_int(value, 32000); + gst_value_list_prepend_value(list, value); + } + if (sbc->frequency & BT_SBC_SAMPLING_FREQ_16000) { + g_value_set_int(value, 16000); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "rate", list); + g_free(list); + list = NULL; + } + + /* bitpool */ + value = g_value_init(value, GST_TYPE_INT_RANGE); + gst_value_set_int_range(value, + MIN(sbc->min_bitpool, TEMPLATE_MAX_BITPOOL), + MIN(sbc->max_bitpool, TEMPLATE_MAX_BITPOOL)); + gst_structure_set_value(structure, "bitpool", value); + g_value_unset(value); + + /* channels */ + mono = FALSE; + stereo = FALSE; + if (sbc->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) + mono = TRUE; + if ((sbc->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) || + (sbc->channel_mode & + BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) || + (sbc->channel_mode & + BT_A2DP_CHANNEL_MODE_JOINT_STEREO)) + stereo = TRUE; + + if (mono && stereo) { + g_value_init(value, GST_TYPE_INT_RANGE); + gst_value_set_int_range(value, 1, 2); + } else { + g_value_init(value, G_TYPE_INT); + if (mono) + g_value_set_int(value, 1); + else if (stereo) + g_value_set_int(value, 2); + else { + GST_ERROR_OBJECT(self, + "Unexpected number of channels"); + g_value_set_int(value, 0); + } + } + + gst_structure_set_value(structure, "channels", value); + g_free(value); + + return structure; +} + +static GstStructure *gst_avdtp_sink_parse_mpeg_raw(GstAvdtpSink *self) +{ + a2dp_mpeg_t *mpeg = (a2dp_mpeg_t *) self->data->config; + GstStructure *structure; + GValue *value; + GValue *list; + gboolean valid_layer = FALSE; + gboolean mono, stereo; + + GST_LOG_OBJECT(self, "parsing mpeg caps"); + + structure = gst_structure_empty_new("audio/mpeg"); + value = g_new0(GValue, 1); + g_value_init(value, G_TYPE_INT); + + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + g_value_set_int(value, 1); + gst_value_list_prepend_value(list, value); + g_value_set_int(value, 2); + gst_value_list_prepend_value(list, value); + gst_structure_set_value(structure, "mpegversion", list); + g_free(list); + + /* layer */ + GST_LOG_OBJECT(self, "setting mpeg layer"); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (mpeg->layer & BT_MPEG_LAYER_1) { + g_value_set_int(value, 1); + gst_value_list_prepend_value(list, value); + valid_layer = TRUE; + } + if (mpeg->layer & BT_MPEG_LAYER_2) { + g_value_set_int(value, 2); + gst_value_list_prepend_value(list, value); + valid_layer = TRUE; + } + if (mpeg->layer & BT_MPEG_LAYER_3) { + g_value_set_int(value, 3); + gst_value_list_prepend_value(list, value); + valid_layer = TRUE; + } + if (list) { + gst_structure_set_value(structure, "layer", list); + g_free(list); + list = NULL; + } + + if (!valid_layer) { + gst_structure_free(structure); + g_free(value); + return NULL; + } + + /* rate */ + GST_LOG_OBJECT(self, "setting mpeg rate"); + list = g_value_init(g_new0(GValue, 1), GST_TYPE_LIST); + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_48000) { + g_value_set_int(value, 48000); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_44100) { + g_value_set_int(value, 44100); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_32000) { + g_value_set_int(value, 32000); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_24000) { + g_value_set_int(value, 24000); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_22050) { + g_value_set_int(value, 22050); + gst_value_list_prepend_value(list, value); + } + if (mpeg->frequency & BT_MPEG_SAMPLING_FREQ_16000) { + g_value_set_int(value, 16000); + gst_value_list_prepend_value(list, value); + } + g_value_unset(value); + if (list) { + gst_structure_set_value(structure, "rate", list); + g_free(list); + list = NULL; + } + + /* channels */ + GST_LOG_OBJECT(self, "setting mpeg channels"); + mono = FALSE; + stereo = FALSE; + if (mpeg->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) + mono = TRUE; + if ((mpeg->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) || + (mpeg->channel_mode & + BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) || + (mpeg->channel_mode & + BT_A2DP_CHANNEL_MODE_JOINT_STEREO)) + stereo = TRUE; + + if (mono && stereo) { + g_value_init(value, GST_TYPE_INT_RANGE); + gst_value_set_int_range(value, 1, 2); + } else { + g_value_init(value, G_TYPE_INT); + if (mono) + g_value_set_int(value, 1); + else if (stereo) + g_value_set_int(value, 2); + else { + GST_ERROR_OBJECT(self, + "Unexpected number of channels"); + g_value_set_int(value, 0); + } + } + gst_structure_set_value(structure, "channels", value); + g_free(value); + + return structure; +} + +static gboolean gst_avdtp_sink_update_config(GstAvdtpSink *self) +{ + GstStructure *structure; + gchar *tmp; + + switch (self->data->codec) { + case A2DP_CODEC_SBC: + structure = gst_avdtp_sink_parse_sbc_raw(self); + break; + case A2DP_CODEC_MPEG12: + structure = gst_avdtp_sink_parse_mpeg_raw(self); + break; + default: + GST_ERROR_OBJECT(self, "Unsupported configuration"); + return FALSE; + } + + if (structure == NULL) + return FALSE; + + if (self->dev_caps != NULL) + gst_caps_unref(self->dev_caps); + + self->dev_caps = gst_caps_new_full(structure, NULL); + + tmp = gst_caps_to_string(self->dev_caps); + GST_DEBUG_OBJECT(self, "Transport configuration: %s", tmp); + g_free(tmp); + + return TRUE; +} + +static gboolean gst_avdtp_sink_update_caps(GstAvdtpSink *self) +{ + sbc_capabilities_t *sbc; + mpeg_capabilities_t *mpeg; + GstStructure *sbc_structure; + GstStructure *mpeg_structure; + gchar *tmp; + + GST_LOG_OBJECT(self, "updating device caps"); + + if (self->data->config_size != 0 && self->data->config != NULL) + return gst_avdtp_sink_update_config(self); + + sbc = (void *) gst_avdtp_find_caps(self, BT_A2DP_SBC_SINK); + mpeg = (void *) gst_avdtp_find_caps(self, BT_A2DP_MPEG12_SINK); + + if (!sbc) { + GST_ERROR_OBJECT(self, "Failed to find mandatory SBC sink"); + return FALSE; + } + + sbc_structure = gst_avdtp_sink_parse_sbc_caps(self, sbc); + mpeg_structure = gst_avdtp_sink_parse_mpeg_caps(self, mpeg); + + if (self->dev_caps != NULL) + gst_caps_unref(self->dev_caps); + self->dev_caps = gst_caps_new_full(sbc_structure, NULL); + if (mpeg_structure != NULL) + gst_caps_append_structure(self->dev_caps, mpeg_structure); + + tmp = gst_caps_to_string(self->dev_caps); + GST_DEBUG_OBJECT(self, "Device capabilities: %s", tmp); + g_free(tmp); + + return TRUE; +} + +static gboolean gst_avdtp_sink_get_capabilities(GstAvdtpSink *self) +{ + gchar buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_get_capabilities_req *req = (void *) buf; + struct bt_get_capabilities_rsp *rsp = (void *) buf; + int err; + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + + req->h.type = BT_REQUEST; + req->h.name = BT_GET_CAPABILITIES; + req->h.length = sizeof(*req); + + if (self->device == NULL) + return FALSE; + strncpy(req->destination, self->device, 18); + if (self->autoconnect) + req->flags |= BT_FLAG_AUTOCONNECT; + + err = gst_avdtp_sink_audioservice_send(self, &req->h); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error while asking device caps"); + return FALSE; + } + + rsp->h.length = 0; + err = gst_avdtp_sink_audioservice_expect(self, + &rsp->h, BT_GET_CAPABILITIES); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error while getting device caps"); + return FALSE; + } + + self->data->caps = g_malloc0(rsp->h.length); + memcpy(self->data->caps, rsp, rsp->h.length); + if (!gst_avdtp_sink_update_caps(self)) { + GST_WARNING_OBJECT(self, "failed to update capabilities"); + return FALSE; + } + + return TRUE; +} + +static gint gst_avdtp_sink_get_channel_mode(const gchar *mode) +{ + if (strcmp(mode, "stereo") == 0) + return BT_A2DP_CHANNEL_MODE_STEREO; + else if (strcmp(mode, "joint-stereo") == 0) + return BT_A2DP_CHANNEL_MODE_JOINT_STEREO; + else if (strcmp(mode, "dual-channel") == 0) + return BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL; + else if (strcmp(mode, "mono") == 0) + return BT_A2DP_CHANNEL_MODE_MONO; + else + return -1; +} + +static void gst_avdtp_sink_tag(const GstTagList *taglist, + const gchar *tag, gpointer user_data) +{ + gboolean crc; + gchar *channel_mode = NULL; + GstAvdtpSink *self = GST_AVDTP_SINK(user_data); + + if (strcmp(tag, "has-crc") == 0) { + + if (!gst_tag_list_get_boolean(taglist, tag, &crc)) { + GST_WARNING_OBJECT(self, "failed to get crc tag"); + return; + } + + gst_avdtp_sink_set_crc(self, crc); + + } else if (strcmp(tag, "channel-mode") == 0) { + + if (!gst_tag_list_get_string(taglist, tag, &channel_mode)) { + GST_WARNING_OBJECT(self, + "failed to get channel-mode tag"); + return; + } + + self->channel_mode = gst_avdtp_sink_get_channel_mode( + channel_mode); + if (self->channel_mode == -1) + GST_WARNING_OBJECT(self, "Received invalid channel " + "mode: %s", channel_mode); + g_free(channel_mode); + + } else + GST_DEBUG_OBJECT(self, "received unused tag: %s", tag); +} + +static gboolean gst_avdtp_sink_event(GstBaseSink *basesink, + GstEvent *event) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(basesink); + GstTagList *taglist = NULL; + + if (GST_EVENT_TYPE(event) == GST_EVENT_TAG) { + /* we check the tags, mp3 has tags that are importants and + * are outside caps */ + gst_event_parse_tag(event, &taglist); + gst_tag_list_foreach(taglist, gst_avdtp_sink_tag, self); + } + + return TRUE; +} + +static gboolean gst_avdtp_sink_transport_parse_property(GstAvdtpSink *self, + DBusMessageIter *i) +{ + const char *key; + DBusMessageIter variant_i; + + if (dbus_message_iter_get_arg_type(i) != DBUS_TYPE_STRING) { + GST_ERROR_OBJECT(self, "Property name not a string."); + return FALSE; + } + + dbus_message_iter_get_basic(i, &key); + + if (!dbus_message_iter_next(i)) { + GST_ERROR_OBJECT(self, "Property value missing"); + return FALSE; + } + + if (dbus_message_iter_get_arg_type(i) != DBUS_TYPE_VARIANT) { + GST_ERROR_OBJECT(self, "Property value not a variant."); + return FALSE; + } + + dbus_message_iter_recurse(i, &variant_i); + + switch (dbus_message_iter_get_arg_type(&variant_i)) { + case DBUS_TYPE_BYTE: { + uint8_t value; + dbus_message_iter_get_basic(&variant_i, &value); + + if (g_str_equal(key, "Codec") == TRUE) + self->data->codec = value; + + break; + } + case DBUS_TYPE_STRING: { + const char *value; + dbus_message_iter_get_basic(&variant_i, &value); + + if (g_str_equal(key, "UUID") == TRUE) { + g_free(self->data->uuid); + self->data->uuid = g_strdup(value); + } + + break; + } + case DBUS_TYPE_ARRAY: { + DBusMessageIter array_i; + char *value; + int size; + + dbus_message_iter_recurse(&variant_i, &array_i); + dbus_message_iter_get_fixed_array(&array_i, &value, &size); + + if (g_str_equal(key, "Configuration")) { + g_free(self->data->config); + self->data->config = g_new0(guint8, size); + self->data->config_size = size; + memcpy(self->data->config, value, size); + } + + break; + } + } + + return TRUE; +} + +static gboolean gst_avdtp_sink_transport_acquire(GstAvdtpSink *self) +{ + DBusMessage *msg, *reply; + DBusError err; + const char *access_type = "w"; + int fd; + uint16_t imtu, omtu; + + dbus_error_init(&err); + + if (self->data->conn == NULL) + self->data->conn = dbus_bus_get(DBUS_BUS_SYSTEM, &err); + + msg = dbus_message_new_method_call("org.bluez", self->transport, + "org.bluez.MediaTransport", + "Acquire"); + + dbus_message_append_args(msg, DBUS_TYPE_STRING, &access_type, + DBUS_TYPE_INVALID); + + reply = dbus_connection_send_with_reply_and_block(self->data->conn, + msg, -1, &err); + + dbus_message_unref(msg); + + if (dbus_error_is_set(&err)) + goto fail; + + if (dbus_message_get_args(reply, &err, DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_UINT16, &imtu, + DBUS_TYPE_UINT16, &omtu, + DBUS_TYPE_INVALID) == FALSE) + goto fail; + + dbus_message_unref(reply); + + self->stream = g_io_channel_unix_new(fd); + g_io_channel_set_encoding(self->stream, NULL, NULL); + g_io_channel_set_close_on_unref(self->stream, TRUE); + self->data->link_mtu = omtu; + GST_DEBUG_OBJECT(self, "stream_fd=%d mtu=%d", fd, omtu); + + return TRUE; + +fail: + GST_ERROR_OBJECT(self, "Failed to acquire transport stream: %s", + err.message); + + dbus_error_free(&err); + + if (reply) + dbus_message_unref(reply); + + return FALSE; +} + +static gboolean gst_avdtp_sink_transport_get_properties(GstAvdtpSink *self) +{ + DBusMessage *msg, *reply; + DBusMessageIter arg_i, ele_i; + DBusError err; + + dbus_error_init(&err); + + /* Transport need to be acquire first to make sure the MTUs are + available */ + if (gst_avdtp_sink_transport_acquire(self) == FALSE) + return FALSE; + + msg = dbus_message_new_method_call("org.bluez", self->transport, + "org.bluez.MediaTransport", + "GetProperties"); + reply = dbus_connection_send_with_reply_and_block(self->data->conn, + msg, -1, &err); + + if (dbus_error_is_set(&err) || reply == NULL) { + GST_ERROR_OBJECT(self, "Failed to get transport properties: %s", + err.message); + goto fail; + } + + if (!dbus_message_iter_init(reply, &arg_i)) { + GST_ERROR_OBJECT(self, "GetProperties reply has no arguments."); + goto fail; + } + + if (dbus_message_iter_get_arg_type(&arg_i) != DBUS_TYPE_ARRAY) { + GST_ERROR_OBJECT(self, "GetProperties argument is not an array."); + goto fail; + } + + dbus_message_iter_recurse(&arg_i, &ele_i); + while (dbus_message_iter_get_arg_type(&ele_i) != DBUS_TYPE_INVALID) { + + if (dbus_message_iter_get_arg_type(&ele_i) == + DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_i; + + dbus_message_iter_recurse(&ele_i, &dict_i); + + gst_avdtp_sink_transport_parse_property(self, &dict_i); + } + + if (!dbus_message_iter_next(&ele_i)) + break; + } + + return gst_avdtp_sink_update_caps(self); + +fail: + dbus_message_unref(msg); + dbus_message_unref(reply); + return FALSE; + +} + +static gboolean gst_avdtp_sink_start(GstBaseSink *basesink) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(basesink); + gint sk; + gint err; + + GST_INFO_OBJECT(self, "start"); + + self->data = g_new0(struct bluetooth_data, 1); + + self->stream = NULL; + self->stream_caps = NULL; + self->mp3_using_crc = -1; + self->channel_mode = -1; + + if (self->transport != NULL) + return gst_avdtp_sink_transport_get_properties(self); + + self->watch_id = 0; + + sk = bt_audio_service_open(); + if (sk < 0) { + err = -errno; + GST_ERROR_OBJECT(self, "Cannot open connection to bt " + "audio service: %s %d", strerror(-err), -err); + return FALSE; + } + + self->server = g_io_channel_unix_new(sk); + g_io_channel_set_encoding(self->server, NULL, NULL); + self->watch_id = g_io_add_watch(self->server, G_IO_HUP | G_IO_ERR | + G_IO_NVAL, server_callback, self); + + if (!gst_avdtp_sink_get_capabilities(self)) { + GST_ERROR_OBJECT(self, "failed to get capabilities " + "from device"); + goto failed; + } + + return TRUE; + +failed: + bt_audio_service_close(sk); + return FALSE; +} + +static gboolean gst_avdtp_sink_stream_start(GstAvdtpSink *self) +{ + gchar buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_start_stream_req *req = (void *) buf; + struct bt_start_stream_rsp *rsp = (void *) buf; + struct bt_new_stream_ind *ind = (void *) buf; + int err; + + if (self->transport != NULL) + return gst_avdtp_sink_conf_recv_stream_fd(self); + + memset(req, 0, sizeof(buf)); + req->h.type = BT_REQUEST; + req->h.name = BT_START_STREAM; + req->h.length = sizeof(*req); + + err = gst_avdtp_sink_audioservice_send(self, &req->h); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error occurred while sending " + "start packet"); + return FALSE; + } + + rsp->h.length = sizeof(*rsp); + err = gst_avdtp_sink_audioservice_expect(self, &rsp->h, + BT_START_STREAM); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error while stream " + "start confirmation"); + return FALSE; + } + + ind->h.length = sizeof(*ind); + err = gst_avdtp_sink_audioservice_expect(self, &ind->h, + BT_NEW_STREAM); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error while receiving " + "stream filedescriptor"); + return FALSE; + } + + if (!gst_avdtp_sink_conf_recv_stream_fd(self)) + return FALSE; + + return TRUE; +} + +static gboolean gst_avdtp_sink_init_mp3_pkt_conf( + GstAvdtpSink *self, GstCaps *caps, + mpeg_capabilities_t *pkt) +{ + const GValue *value = NULL; + gint rate, layer; + const gchar *name; + GstStructure *structure = gst_caps_get_structure(caps, 0); + + name = gst_structure_get_name(structure); + + if (!(IS_MPEG_AUDIO(name))) { + GST_ERROR_OBJECT(self, "Unexpected format %s, " + "was expecting mp3", name); + return FALSE; + } + + /* layer */ + value = gst_structure_get_value(structure, "layer"); + layer = g_value_get_int(value); + if (layer == 1) + pkt->layer = BT_MPEG_LAYER_1; + else if (layer == 2) + pkt->layer = BT_MPEG_LAYER_2; + else if (layer == 3) + pkt->layer = BT_MPEG_LAYER_3; + else { + GST_ERROR_OBJECT(self, "Unexpected layer: %d", layer); + return FALSE; + } + + /* crc */ + if (self->mp3_using_crc != -1) + pkt->crc = self->mp3_using_crc; + else { + GST_ERROR_OBJECT(self, "No info about crc was received, " + " can't proceed"); + return FALSE; + } + + /* channel mode */ + if (self->channel_mode != -1) + pkt->channel_mode = self->channel_mode; + else { + GST_ERROR_OBJECT(self, "No info about channel mode " + "received, can't proceed"); + return FALSE; + } + + /* mpf - we will only use the mandatory one */ + pkt->mpf = 0; + + value = gst_structure_get_value(structure, "rate"); + rate = g_value_get_int(value); + if (rate == 44100) + pkt->frequency = BT_MPEG_SAMPLING_FREQ_44100; + else if (rate == 48000) + pkt->frequency = BT_MPEG_SAMPLING_FREQ_48000; + else if (rate == 32000) + pkt->frequency = BT_MPEG_SAMPLING_FREQ_32000; + else if (rate == 24000) + pkt->frequency = BT_MPEG_SAMPLING_FREQ_24000; + else if (rate == 22050) + pkt->frequency = BT_MPEG_SAMPLING_FREQ_22050; + else if (rate == 16000) + pkt->frequency = BT_MPEG_SAMPLING_FREQ_16000; + else { + GST_ERROR_OBJECT(self, "Invalid rate while setting caps"); + return FALSE; + } + + /* vbr - we always say its vbr, we don't have how to know it */ + pkt->bitrate = 0x8000; + + return TRUE; +} + +static gboolean gst_avdtp_sink_configure(GstAvdtpSink *self, + GstCaps *caps) +{ + gchar buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_open_req *open_req = (void *) buf; + struct bt_open_rsp *open_rsp = (void *) buf; + struct bt_set_configuration_req *req = (void *) buf; + struct bt_set_configuration_rsp *rsp = (void *) buf; + gboolean ret; + gchar *temp; + GstStructure *structure; + codec_capabilities_t *codec = NULL; + int err; + + temp = gst_caps_to_string(caps); + GST_DEBUG_OBJECT(self, "configuring device with caps: %s", temp); + g_free(temp); + + /* Transport already configured */ + if (self->transport != NULL) + return TRUE; + + structure = gst_caps_get_structure(caps, 0); + + if (gst_structure_has_name(structure, "audio/x-sbc")) + codec = (void *) gst_avdtp_find_caps(self, BT_A2DP_SBC_SINK); + else if (gst_structure_has_name(structure, "audio/mpeg")) + codec = (void *) gst_avdtp_find_caps(self, BT_A2DP_MPEG12_SINK); + + if (codec == NULL) { + GST_ERROR_OBJECT(self, "Couldn't parse caps " + "to packet configuration"); + return FALSE; + } + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + open_req->h.type = BT_REQUEST; + open_req->h.name = BT_OPEN; + open_req->h.length = sizeof(*open_req); + + strncpy(open_req->destination, self->device, 18); + open_req->seid = codec->seid; + open_req->lock = BT_WRITE_LOCK; + + err = gst_avdtp_sink_audioservice_send(self, &open_req->h); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error occurred while sending " + "open packet"); + return FALSE; + } + + open_rsp->h.length = sizeof(*open_rsp); + err = gst_avdtp_sink_audioservice_expect(self, &open_rsp->h, + BT_OPEN); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error while receiving device " + "confirmation"); + return FALSE; + } + + memset(req, 0, sizeof(buf)); + req->h.type = BT_REQUEST; + req->h.name = BT_SET_CONFIGURATION; + req->h.length = sizeof(*req); + memcpy(&req->codec, codec, sizeof(req->codec)); + + if (codec->type == BT_A2DP_SBC_SINK) + ret = gst_avdtp_sink_init_sbc_pkt_conf(self, caps, + (void *) &req->codec); + else + ret = gst_avdtp_sink_init_mp3_pkt_conf(self, caps, + (void *) &req->codec); + + if (!ret) { + GST_ERROR_OBJECT(self, "Couldn't parse caps " + "to packet configuration"); + return FALSE; + } + + req->h.length += req->codec.length - sizeof(req->codec); + err = gst_avdtp_sink_audioservice_send(self, &req->h); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error occurred while sending " + "configurarion packet"); + return FALSE; + } + + rsp->h.length = sizeof(*rsp); + err = gst_avdtp_sink_audioservice_expect(self, &rsp->h, + BT_SET_CONFIGURATION); + if (err < 0) { + GST_ERROR_OBJECT(self, "Error while receiving device " + "confirmation"); + return FALSE; + } + + self->data->link_mtu = rsp->link_mtu; + + return TRUE; +} + +static GstFlowReturn gst_avdtp_sink_preroll(GstBaseSink *basesink, + GstBuffer *buffer) +{ + GstAvdtpSink *sink = GST_AVDTP_SINK(basesink); + gboolean ret; + + GST_AVDTP_SINK_MUTEX_LOCK(sink); + + ret = gst_avdtp_sink_stream_start(sink); + + GST_AVDTP_SINK_MUTEX_UNLOCK(sink); + + if (!ret) + return GST_FLOW_ERROR; + + return GST_FLOW_OK; +} + +static GstFlowReturn gst_avdtp_sink_render(GstBaseSink *basesink, + GstBuffer *buffer) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(basesink); + ssize_t ret; + int fd; + + fd = g_io_channel_unix_get_fd(self->stream); + + ret = write(fd, GST_BUFFER_DATA(buffer), GST_BUFFER_SIZE(buffer)); + if (ret < 0) { + GST_ERROR_OBJECT(self, "Error while writting to socket: %s", + strerror(errno)); + return GST_FLOW_ERROR; + } + + return GST_FLOW_OK; +} + +static gboolean gst_avdtp_sink_unlock(GstBaseSink *basesink) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(basesink); + + if (self->stream != NULL) + g_io_channel_flush(self->stream, NULL); + + return TRUE; +} + +static GstFlowReturn gst_avdtp_sink_buffer_alloc(GstBaseSink *basesink, + guint64 offset, guint size, GstCaps *caps, + GstBuffer **buf) +{ + GstAvdtpSink *self = GST_AVDTP_SINK(basesink); + + *buf = gst_buffer_new_and_alloc(size); + if (!(*buf)) { + GST_ERROR_OBJECT(self, "buffer allocation failed"); + return GST_FLOW_ERROR; + } + + gst_buffer_set_caps(*buf, caps); + + GST_BUFFER_OFFSET(*buf) = offset; + + return GST_FLOW_OK; +} + +static void gst_avdtp_sink_class_init(GstAvdtpSinkClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GstBaseSinkClass *basesink_class = GST_BASE_SINK_CLASS(klass); + + parent_class = g_type_class_peek_parent(klass); + + object_class->finalize = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_finalize); + object_class->set_property = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_set_property); + object_class->get_property = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_get_property); + + basesink_class->start = GST_DEBUG_FUNCPTR(gst_avdtp_sink_start); + basesink_class->stop = GST_DEBUG_FUNCPTR(gst_avdtp_sink_stop); + basesink_class->render = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_render); + basesink_class->preroll = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_preroll); + basesink_class->unlock = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_unlock); + basesink_class->event = GST_DEBUG_FUNCPTR( + gst_avdtp_sink_event); + + basesink_class->buffer_alloc = + GST_DEBUG_FUNCPTR(gst_avdtp_sink_buffer_alloc); + + g_object_class_install_property(object_class, PROP_DEVICE, + g_param_spec_string("device", "Device", + "Bluetooth remote device address", + NULL, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_AUTOCONNECT, + g_param_spec_boolean("auto-connect", + "Auto-connect", + "Automatically attempt to connect " + "to device", DEFAULT_AUTOCONNECT, + G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_TRANSPORT, + g_param_spec_string("transport", + "Transport", + "Use configured transport", + NULL, G_PARAM_READWRITE)); + + GST_DEBUG_CATEGORY_INIT(avdtp_sink_debug, "avdtpsink", 0, + "A2DP headset sink element"); +} + +static void gst_avdtp_sink_init(GstAvdtpSink *self, + GstAvdtpSinkClass *klass) +{ + self->device = NULL; + self->transport = NULL; + self->data = NULL; + + self->stream = NULL; + + self->dev_caps = NULL; + + self->autoconnect = DEFAULT_AUTOCONNECT; + + self->sink_lock = g_mutex_new(); + + /* FIXME this is for not synchronizing with clock, should be tested + * with devices to see the behaviour + gst_base_sink_set_sync(GST_BASE_SINK(self), FALSE); + */ +} + +static int gst_avdtp_sink_audioservice_send(GstAvdtpSink *self, + const bt_audio_msg_header_t *msg) +{ + ssize_t written; + const char *type, *name; + uint16_t length; + int fd, err; + + length = msg->length ? msg->length : BT_SUGGESTED_BUFFER_SIZE; + + fd = g_io_channel_unix_get_fd(self->server); + + written = write(fd, msg, length); + if (written < 0) { + err = -errno; + GST_ERROR_OBJECT(self, "Error sending data to audio service:" + " %s", strerror(-err)); + return err; + } + + type = bt_audio_strtype(msg->type); + name = bt_audio_strname(msg->name); + + GST_DEBUG_OBJECT(self, "sent: %s -> %s", type, name); + + return 0; +} + +static int gst_avdtp_sink_audioservice_recv(GstAvdtpSink *self, + bt_audio_msg_header_t *inmsg) +{ + ssize_t bytes_read; + const char *type, *name; + uint16_t length; + int fd, err = 0; + + length = inmsg->length ? inmsg->length : BT_SUGGESTED_BUFFER_SIZE; + + fd = g_io_channel_unix_get_fd(self->server); + + bytes_read = read(fd, inmsg, length); + if (bytes_read < 0) { + err = -errno; + GST_ERROR_OBJECT(self, "Error receiving data from " + "audio service: %s", strerror(-err)); + return err; + } + + type = bt_audio_strtype(inmsg->type); + if (!type) { + err = -EINVAL; + GST_ERROR_OBJECT(self, "Bogus message type %d " + "received from audio service", + inmsg->type); + } + + name = bt_audio_strname(inmsg->name); + if (!name) { + err = -EINVAL; + GST_ERROR_OBJECT(self, "Bogus message name %d " + "received from audio service", + inmsg->name); + } + + if (inmsg->type == BT_ERROR) { + bt_audio_error_t *msg = (void *) inmsg; + err = -EINVAL; + GST_ERROR_OBJECT(self, "%s failed : " + "%s(%d)", + name, + strerror(msg->posix_errno), + msg->posix_errno); + } + + GST_DEBUG_OBJECT(self, "received: %s <- %s", type, name); + + return err; +} + +static int gst_avdtp_sink_audioservice_expect(GstAvdtpSink *self, + bt_audio_msg_header_t *outmsg, + guint8 expected_name) +{ + int err; + + err = gst_avdtp_sink_audioservice_recv(self, outmsg); + if (err < 0) + return err; + + if (outmsg->name != expected_name) + return -EINVAL; + + return 0; +} + +gboolean gst_avdtp_sink_plugin_init(GstPlugin *plugin) +{ + return gst_element_register(plugin, "avdtpsink", GST_RANK_NONE, + GST_TYPE_AVDTP_SINK); +} + + +/* public functions */ +GstCaps *gst_avdtp_sink_get_device_caps(GstAvdtpSink *sink) +{ + if (sink->dev_caps == NULL) + return NULL; + + return gst_caps_copy(sink->dev_caps); +} + +gboolean gst_avdtp_sink_set_device_caps(GstAvdtpSink *self, + GstCaps *caps) +{ + gboolean ret; + + GST_DEBUG_OBJECT(self, "setting device caps"); + GST_AVDTP_SINK_MUTEX_LOCK(self); + ret = gst_avdtp_sink_configure(self, caps); + + if (self->stream_caps) + gst_caps_unref(self->stream_caps); + self->stream_caps = gst_caps_ref(caps); + + GST_AVDTP_SINK_MUTEX_UNLOCK(self); + + return ret; +} + +guint gst_avdtp_sink_get_link_mtu(GstAvdtpSink *sink) +{ + return sink->data->link_mtu; +} + +void gst_avdtp_sink_set_device(GstAvdtpSink *self, const gchar *dev) +{ + if (self->device != NULL) + g_free(self->device); + + GST_LOG_OBJECT(self, "Setting device: %s", dev); + self->device = g_strdup(dev); +} + +void gst_avdtp_sink_set_transport(GstAvdtpSink *self, const gchar *trans) +{ + if (self->transport != NULL) + g_free(self->transport); + + GST_LOG_OBJECT(self, "Setting transport: %s", trans); + self->transport = g_strdup(trans); +} + +gchar *gst_avdtp_sink_get_device(GstAvdtpSink *self) +{ + return g_strdup(self->device); +} + +gchar *gst_avdtp_sink_get_transport(GstAvdtpSink *self) +{ + return g_strdup(self->transport); +} + +void gst_avdtp_sink_set_crc(GstAvdtpSink *self, gboolean crc) +{ + gint new_crc; + + new_crc = crc ? CRC_PROTECTED : CRC_UNPROTECTED; + + /* test if we already received a different crc */ + if (self->mp3_using_crc != -1 && new_crc != self->mp3_using_crc) { + GST_WARNING_OBJECT(self, "crc changed during stream"); + return; + } + self->mp3_using_crc = new_crc; + +} + +void gst_avdtp_sink_set_channel_mode(GstAvdtpSink *self, + const gchar *mode) +{ + gint new_mode; + + new_mode = gst_avdtp_sink_get_channel_mode(mode); + + if (self->channel_mode != -1 && new_mode != self->channel_mode) { + GST_WARNING_OBJECT(self, "channel mode changed during stream"); + return; + } + + self->channel_mode = new_mode; + if (self->channel_mode == -1) + GST_WARNING_OBJECT(self, "Received invalid channel " + "mode: %s", mode); +} diff --git a/audio/gstavdtpsink.h b/audio/gstavdtpsink.h new file mode 100644 index 0000000..c4e5645 --- /dev/null +++ b/audio/gstavdtpsink.h @@ -0,0 +1,107 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __GST_AVDTP_SINK_H +#define __GST_AVDTP_SINK_H + +#include +#include + +G_BEGIN_DECLS + +#define GST_TYPE_AVDTP_SINK \ + (gst_avdtp_sink_get_type()) +#define GST_AVDTP_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_AVDTP_SINK,\ + GstAvdtpSink)) +#define GST_AVDTP_SINK_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_AVDTP_SINK,\ + GstAvdtpSinkClass)) +#define GST_IS_AVDTP_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_AVDTP_SINK)) +#define GST_IS_AVDTP_SINK_CLASS(obj) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_AVDTP_SINK)) + +typedef struct _GstAvdtpSink GstAvdtpSink; +typedef struct _GstAvdtpSinkClass GstAvdtpSinkClass; + +struct bluetooth_data; + +struct _GstAvdtpSink { + GstBaseSink sink; + + gchar *device; + gchar *transport; + GIOChannel *stream; + + struct bluetooth_data *data; + gboolean autoconnect; + GIOChannel *server; + + /* mp3 stream data (outside caps data)*/ + gint mp3_using_crc; + gint channel_mode; + + /* stream connection data */ + GstCaps *stream_caps; + + GstCaps *dev_caps; + + GMutex *sink_lock; + + guint watch_id; +}; + +struct _GstAvdtpSinkClass { + GstBaseSinkClass parent_class; +}; + +GType gst_avdtp_sink_get_type(void); + +GstCaps *gst_avdtp_sink_get_device_caps(GstAvdtpSink *sink); +gboolean gst_avdtp_sink_set_device_caps(GstAvdtpSink *sink, + GstCaps *caps); + +guint gst_avdtp_sink_get_link_mtu(GstAvdtpSink *sink); + +void gst_avdtp_sink_set_device(GstAvdtpSink *sink, + const gchar* device); + +void gst_avdtp_sink_set_transport(GstAvdtpSink *sink, + const gchar *transport); + +gchar *gst_avdtp_sink_get_device(GstAvdtpSink *sink); + +gchar *gst_avdtp_sink_get_transport(GstAvdtpSink *sink); + +gboolean gst_avdtp_sink_plugin_init(GstPlugin *plugin); + +void gst_avdtp_sink_set_crc(GstAvdtpSink *self, gboolean crc); + +void gst_avdtp_sink_set_channel_mode(GstAvdtpSink *self, + const gchar *mode); + + +G_END_DECLS + +#endif /* __GST_AVDTP_SINK_H */ diff --git a/audio/gstbluetooth.c b/audio/gstbluetooth.c new file mode 100644 index 0000000..9930820 --- /dev/null +++ b/audio/gstbluetooth.c @@ -0,0 +1,109 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "gstsbcutil.h" +#include + +#include "gstsbcenc.h" +#include "gstsbcdec.h" +#include "gstsbcparse.h" +#include "gstavdtpsink.h" +#include "gsta2dpsink.h" +#include "gstrtpsbcpay.h" + +static GstStaticCaps sbc_caps = GST_STATIC_CAPS("audio/x-sbc"); + +#define SBC_CAPS (gst_static_caps_get(&sbc_caps)) + +static void sbc_typefind(GstTypeFind *tf, gpointer ignore) +{ + GstCaps *caps; + guint8 *aux; + sbc_t sbc; + guint8 *data = gst_type_find_peek(tf, 0, 32); + + if (data == NULL) + return; + + if (sbc_init(&sbc, 0) < 0) + return; + + aux = g_new(guint8, 32); + memcpy(aux, data, 32); + if (sbc_parse(&sbc, aux, 32) < 0) + goto done; + + caps = gst_sbc_parse_caps_from_sbc(&sbc); + gst_type_find_suggest(tf, GST_TYPE_FIND_POSSIBLE, caps); + gst_caps_unref(caps); + +done: + g_free(aux); + sbc_finish(&sbc); +} + +static gchar *sbc_exts[] = { "sbc", NULL }; + +static gboolean plugin_init(GstPlugin *plugin) +{ + GST_INFO("Bluetooth plugin %s", VERSION); + + if (gst_type_find_register(plugin, "sbc", + GST_RANK_PRIMARY, sbc_typefind, sbc_exts, + SBC_CAPS, NULL, NULL) == FALSE) + return FALSE; + + if (!gst_sbc_enc_plugin_init(plugin)) + return FALSE; + + if (!gst_sbc_dec_plugin_init(plugin)) + return FALSE; + + if (!gst_sbc_parse_plugin_init(plugin)) + return FALSE; + + if (!gst_avdtp_sink_plugin_init(plugin)) + return FALSE; + + if (!gst_a2dp_sink_plugin_init(plugin)) + return FALSE; + + if (!gst_rtp_sbc_pay_plugin_init(plugin)) + return FALSE; + + return TRUE; +} + +extern GstPluginDesc gst_plugin_desc __attribute__ ((visibility("default"))); + +GST_PLUGIN_DEFINE(GST_VERSION_MAJOR, GST_VERSION_MINOR, + "bluetooth", "Bluetooth plugin library", + plugin_init, VERSION, "LGPL", "BlueZ", "http://www.bluez.org/") diff --git a/audio/gstpragma.h b/audio/gstpragma.h new file mode 100644 index 0000000..0cb08d2 --- /dev/null +++ b/audio/gstpragma.h @@ -0,0 +1,24 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +/* #pragma GCC diagnostic warning "-Wmissing-declarations" */ diff --git a/audio/gstrtpsbcpay.c b/audio/gstrtpsbcpay.c new file mode 100644 index 0000000..11aa733 --- /dev/null +++ b/audio/gstrtpsbcpay.c @@ -0,0 +1,351 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "gstpragma.h" +#include "gstrtpsbcpay.h" +#include +#include + +#define RTP_SBC_PAYLOAD_HEADER_SIZE 1 +#define DEFAULT_MIN_FRAMES 0 +#define RTP_SBC_HEADER_TOTAL (12 + RTP_SBC_PAYLOAD_HEADER_SIZE) + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct rtp_payload { + guint8 frame_count:4; + guint8 rfa0:1; + guint8 is_last_fragment:1; + guint8 is_first_fragment:1; + guint8 is_fragmented:1; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct rtp_payload { + guint8 is_fragmented:1; + guint8 is_first_fragment:1; + guint8 is_last_fragment:1; + guint8 rfa0:1; + guint8 frame_count:4; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif + +enum { + PROP_0, + PROP_MIN_FRAMES +}; + +GST_DEBUG_CATEGORY_STATIC(gst_rtp_sbc_pay_debug); +#define GST_CAT_DEFAULT gst_rtp_sbc_pay_debug + +GST_BOILERPLATE(GstRtpSBCPay, gst_rtp_sbc_pay, GstBaseRTPPayload, + GST_TYPE_BASE_RTP_PAYLOAD); + +static const GstElementDetails gst_rtp_sbc_pay_details = + GST_ELEMENT_DETAILS("RTP packet payloader", + "Codec/Payloader/Network", + "Payload SBC audio as RTP packets", + "Thiago Sousa Santos " + ""); + +static GstStaticPadTemplate gst_rtp_sbc_pay_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "mode = (string) { \"mono\", \"dual\", \"stereo\", \"joint\" }, " + "blocks = (int) { 4, 8, 12, 16 }, " + "subbands = (int) { 4, 8 }, " + "allocation = (string) { \"snr\", \"loudness\" }, " + "bitpool = (int) [ 2, 64 ]") + ); + +static GstStaticPadTemplate gst_rtp_sbc_pay_src_factory = + GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, + GST_STATIC_CAPS( + "application/x-rtp, " + "media = (string) \"audio\"," + "payload = (int) " GST_RTP_PAYLOAD_DYNAMIC_STRING ", " + "clock-rate = (int) { 16000, 32000, 44100, 48000 }," + "encoding-name = (string) \"SBC\"") + ); + +static void gst_rtp_sbc_pay_set_property(GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec); +static void gst_rtp_sbc_pay_get_property(GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec); + +static gint gst_rtp_sbc_pay_get_frame_len(gint subbands, gint channels, + gint blocks, gint bitpool, const gchar *channel_mode) +{ + gint len; + gint join; + + len = 4 + (4 * subbands * channels)/8; + + if (strcmp(channel_mode, "mono") == 0 || + strcmp(channel_mode, "dual") == 0) + len += ((blocks * channels * bitpool) + 7) / 8; + else { + join = strcmp(channel_mode, "joint") == 0 ? 1 : 0; + len += ((join * subbands + blocks * bitpool) + 7) / 8; + } + + return len; +} + +static gboolean gst_rtp_sbc_pay_set_caps(GstBaseRTPPayload *payload, + GstCaps *caps) +{ + GstRtpSBCPay *sbcpay; + gint rate, subbands, channels, blocks, bitpool; + gint frame_len; + const gchar *channel_mode; + GstStructure *structure; + + sbcpay = GST_RTP_SBC_PAY(payload); + + structure = gst_caps_get_structure(caps, 0); + if (!gst_structure_get_int(structure, "rate", &rate)) + return FALSE; + if (!gst_structure_get_int(structure, "channels", &channels)) + return FALSE; + if (!gst_structure_get_int(structure, "blocks", &blocks)) + return FALSE; + if (!gst_structure_get_int(structure, "bitpool", &bitpool)) + return FALSE; + if (!gst_structure_get_int(structure, "subbands", &subbands)) + return FALSE; + + channel_mode = gst_structure_get_string(structure, "mode"); + if (!channel_mode) + return FALSE; + + frame_len = gst_rtp_sbc_pay_get_frame_len(subbands, channels, blocks, + bitpool, channel_mode); + + sbcpay->frame_length = frame_len; + + gst_basertppayload_set_options(payload, "audio", TRUE, "SBC", rate); + + GST_DEBUG_OBJECT(payload, "calculated frame length: %d ", frame_len); + + return gst_basertppayload_set_outcaps(payload, NULL); +} + +static GstFlowReturn gst_rtp_sbc_pay_flush_buffers(GstRtpSBCPay *sbcpay) +{ + guint available; + guint max_payload; + GstBuffer *outbuf; + guint8 *payload_data; + guint frame_count; + guint payload_length; + struct rtp_payload *payload; + + if (sbcpay->frame_length == 0) { + GST_ERROR_OBJECT(sbcpay, "Frame length is 0"); + return GST_FLOW_ERROR; + } + + available = gst_adapter_available(sbcpay->adapter); + + max_payload = gst_rtp_buffer_calc_payload_len( + GST_BASE_RTP_PAYLOAD_MTU(sbcpay) - RTP_SBC_PAYLOAD_HEADER_SIZE, + 0, 0); + + max_payload = MIN(max_payload, available); + frame_count = max_payload / sbcpay->frame_length; + payload_length = frame_count * sbcpay->frame_length; + if (payload_length == 0) /* Nothing to send */ + return GST_FLOW_OK; + + outbuf = gst_rtp_buffer_new_allocate(payload_length + + RTP_SBC_PAYLOAD_HEADER_SIZE, 0, 0); + + gst_rtp_buffer_set_payload_type(outbuf, + GST_BASE_RTP_PAYLOAD_PT(sbcpay)); + + payload_data = gst_rtp_buffer_get_payload(outbuf); + payload = (struct rtp_payload *) payload_data; + memset(payload, 0, sizeof(struct rtp_payload)); + payload->frame_count = frame_count; + + gst_adapter_copy(sbcpay->adapter, payload_data + + RTP_SBC_PAYLOAD_HEADER_SIZE, 0, payload_length); + gst_adapter_flush(sbcpay->adapter, payload_length); + + GST_BUFFER_TIMESTAMP(outbuf) = sbcpay->timestamp; + GST_DEBUG_OBJECT(sbcpay, "Pushing %d bytes", payload_length); + + return gst_basertppayload_push(GST_BASE_RTP_PAYLOAD(sbcpay), outbuf); +} + +static GstFlowReturn gst_rtp_sbc_pay_handle_buffer(GstBaseRTPPayload *payload, + GstBuffer *buffer) +{ + GstRtpSBCPay *sbcpay; + guint available; + + /* FIXME check for negotiation */ + + sbcpay = GST_RTP_SBC_PAY(payload); + sbcpay->timestamp = GST_BUFFER_TIMESTAMP(buffer); + + gst_adapter_push(sbcpay->adapter, buffer); + + available = gst_adapter_available(sbcpay->adapter); + if (available + RTP_SBC_HEADER_TOTAL >= + GST_BASE_RTP_PAYLOAD_MTU(sbcpay) || + (available > + (sbcpay->min_frames * sbcpay->frame_length))) + return gst_rtp_sbc_pay_flush_buffers(sbcpay); + + return GST_FLOW_OK; +} + +static gboolean gst_rtp_sbc_pay_handle_event(GstPad *pad, + GstEvent *event) +{ + GstRtpSBCPay *sbcpay = GST_RTP_SBC_PAY(GST_PAD_PARENT(pad)); + + switch (GST_EVENT_TYPE(event)) { + case GST_EVENT_EOS: + gst_rtp_sbc_pay_flush_buffers(sbcpay); + break; + default: + break; + } + + return FALSE; +} + +static void gst_rtp_sbc_pay_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&gst_rtp_sbc_pay_sink_factory)); + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&gst_rtp_sbc_pay_src_factory)); + + gst_element_class_set_details(element_class, &gst_rtp_sbc_pay_details); +} + +static void gst_rtp_sbc_pay_finalize(GObject *object) +{ + GstRtpSBCPay *sbcpay = GST_RTP_SBC_PAY(object); + g_object_unref(sbcpay->adapter); + + GST_CALL_PARENT(G_OBJECT_CLASS, finalize, (object)); +} + +static void gst_rtp_sbc_pay_class_init(GstRtpSBCPayClass *klass) +{ + GObjectClass *gobject_class; + GstBaseRTPPayloadClass *payload_class = + GST_BASE_RTP_PAYLOAD_CLASS(klass); + + gobject_class = G_OBJECT_CLASS(klass); + parent_class = g_type_class_peek_parent(klass); + + gobject_class->finalize = GST_DEBUG_FUNCPTR(gst_rtp_sbc_pay_finalize); + gobject_class->set_property = GST_DEBUG_FUNCPTR( + gst_rtp_sbc_pay_set_property); + gobject_class->get_property = GST_DEBUG_FUNCPTR( + gst_rtp_sbc_pay_get_property); + + payload_class->set_caps = GST_DEBUG_FUNCPTR(gst_rtp_sbc_pay_set_caps); + payload_class->handle_buffer = GST_DEBUG_FUNCPTR( + gst_rtp_sbc_pay_handle_buffer); + payload_class->handle_event = GST_DEBUG_FUNCPTR( + gst_rtp_sbc_pay_handle_event); + + /* properties */ + g_object_class_install_property(G_OBJECT_CLASS(klass), + PROP_MIN_FRAMES, + g_param_spec_int("min-frames", "minimum frame number", + "Minimum quantity of frames to send in one packet " + "(-1 for maximum allowed by the mtu)", + -1, G_MAXINT, DEFAULT_MIN_FRAMES, G_PARAM_READWRITE)); + + GST_DEBUG_CATEGORY_INIT(gst_rtp_sbc_pay_debug, "rtpsbcpay", 0, + "RTP SBC payloader"); +} + +static void gst_rtp_sbc_pay_set_property(GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + GstRtpSBCPay *sbcpay; + + sbcpay = GST_RTP_SBC_PAY(object); + + switch (prop_id) { + case PROP_MIN_FRAMES: + sbcpay->min_frames = g_value_get_int(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_rtp_sbc_pay_get_property(GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GstRtpSBCPay *sbcpay; + + sbcpay = GST_RTP_SBC_PAY(object); + + switch (prop_id) { + case PROP_MIN_FRAMES: + g_value_set_int(value, sbcpay->min_frames); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_rtp_sbc_pay_init(GstRtpSBCPay *self, GstRtpSBCPayClass *klass) +{ + self->adapter = gst_adapter_new(); + self->frame_length = 0; + self->timestamp = 0; + + self->min_frames = DEFAULT_MIN_FRAMES; +} + +gboolean gst_rtp_sbc_pay_plugin_init(GstPlugin *plugin) +{ + return gst_element_register(plugin, "rtpsbcpay", GST_RANK_NONE, + GST_TYPE_RTP_SBC_PAY); +} diff --git a/audio/gstrtpsbcpay.h b/audio/gstrtpsbcpay.h new file mode 100644 index 0000000..398de82 --- /dev/null +++ b/audio/gstrtpsbcpay.h @@ -0,0 +1,66 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include +#include +#include + +G_BEGIN_DECLS + +#define GST_TYPE_RTP_SBC_PAY \ + (gst_rtp_sbc_pay_get_type()) +#define GST_RTP_SBC_PAY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_RTP_SBC_PAY,\ + GstRtpSBCPay)) +#define GST_RTP_SBC_PAY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_RTP_SBC_PAY,\ + GstRtpSBCPayClass)) +#define GST_IS_RTP_SBC_PAY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_RTP_SBC_PAY)) +#define GST_IS_RTP_SBC_PAY_CLASS(obj) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_RTP_SBC_PAY)) + +typedef struct _GstRtpSBCPay GstRtpSBCPay; +typedef struct _GstRtpSBCPayClass GstRtpSBCPayClass; + +struct _GstRtpSBCPay { + GstBaseRTPPayload base; + + GstAdapter *adapter; + GstClockTime timestamp; + + guint frame_length; + + guint min_frames; +}; + +struct _GstRtpSBCPayClass { + GstBaseRTPPayloadClass parent_class; +}; + +GType gst_rtp_sbc_pay_get_type(void); + +gboolean gst_rtp_sbc_pay_plugin_init (GstPlugin * plugin); + +G_END_DECLS diff --git a/audio/gstsbcdec.c b/audio/gstsbcdec.c new file mode 100644 index 0000000..c52834e --- /dev/null +++ b/audio/gstsbcdec.c @@ -0,0 +1,221 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "gstpragma.h" +#include "gstsbcutil.h" +#include "gstsbcdec.h" + +GST_DEBUG_CATEGORY_STATIC(sbc_dec_debug); +#define GST_CAT_DEFAULT sbc_dec_debug + +GST_BOILERPLATE(GstSbcDec, gst_sbc_dec, GstElement, GST_TYPE_ELEMENT); + +static const GstElementDetails sbc_dec_details = + GST_ELEMENT_DETAILS("Bluetooth SBC decoder", + "Codec/Decoder/Audio", + "Decode a SBC audio stream", + "Marcel Holtmann "); + +static GstStaticPadTemplate sbc_dec_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc")); + +static GstStaticPadTemplate sbc_dec_src_factory = + GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-raw-int, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "endianness = (int) BYTE_ORDER, " + "signed = (boolean) true, " + "width = (int) 16, " + "depth = (int) 16")); + +static GstFlowReturn sbc_dec_chain(GstPad *pad, GstBuffer *buffer) +{ + GstSbcDec *dec = GST_SBC_DEC(gst_pad_get_parent(pad)); + GstFlowReturn res = GST_FLOW_OK; + guint size, codesize, offset = 0; + guint8 *data; + + codesize = sbc_get_codesize(&dec->sbc); + + if (dec->buffer) { + GstBuffer *temp = buffer; + buffer = gst_buffer_span(dec->buffer, 0, buffer, + GST_BUFFER_SIZE(dec->buffer) + GST_BUFFER_SIZE(buffer)); + gst_buffer_unref(temp); + gst_buffer_unref(dec->buffer); + dec->buffer = NULL; + } + + data = GST_BUFFER_DATA(buffer); + size = GST_BUFFER_SIZE(buffer); + + while (offset < size) { + GstBuffer *output; + GstPadTemplate *template; + GstCaps *caps; + int consumed; + + res = gst_pad_alloc_buffer_and_set_caps(dec->srcpad, + GST_BUFFER_OFFSET_NONE, + codesize, NULL, &output); + + if (res != GST_FLOW_OK) + goto done; + + consumed = sbc_decode(&dec->sbc, data + offset, size - offset, + GST_BUFFER_DATA(output), codesize, + NULL); + if (consumed <= 0) + break; + + /* we will reuse the same caps object */ + if (dec->outcaps == NULL) { + caps = gst_caps_new_simple("audio/x-raw-int", + "rate", G_TYPE_INT, + gst_sbc_parse_rate_from_sbc( + dec->sbc.frequency), + "channels", G_TYPE_INT, + gst_sbc_get_channel_number( + dec->sbc.mode), + NULL); + + template = gst_static_pad_template_get(&sbc_dec_src_factory); + + dec->outcaps = gst_caps_intersect(caps, + gst_pad_template_get_caps(template)); + + gst_caps_unref(caps); + } + + gst_buffer_set_caps(output, dec->outcaps); + + /* FIXME get a real timestamp */ + GST_BUFFER_TIMESTAMP(output) = GST_CLOCK_TIME_NONE; + + res = gst_pad_push(dec->srcpad, output); + if (res != GST_FLOW_OK) + goto done; + + offset += consumed; + } + + if (offset < size) + dec->buffer = gst_buffer_create_sub(buffer, + offset, size - offset); + +done: + gst_buffer_unref(buffer); + gst_object_unref(dec); + + return res; +} + +static GstStateChangeReturn sbc_dec_change_state(GstElement *element, + GstStateChange transition) +{ + GstSbcDec *dec = GST_SBC_DEC(element); + + switch (transition) { + case GST_STATE_CHANGE_READY_TO_PAUSED: + GST_DEBUG("Setup subband codec"); + if (dec->buffer) { + gst_buffer_unref(dec->buffer); + dec->buffer = NULL; + } + sbc_init(&dec->sbc, 0); + dec->outcaps = NULL; + break; + + case GST_STATE_CHANGE_PAUSED_TO_READY: + GST_DEBUG("Finish subband codec"); + if (dec->buffer) { + gst_buffer_unref(dec->buffer); + dec->buffer = NULL; + } + sbc_finish(&dec->sbc); + if (dec->outcaps) { + gst_caps_unref(dec->outcaps); + dec->outcaps = NULL; + } + break; + + default: + break; + } + + return parent_class->change_state(element, transition); +} + +static void gst_sbc_dec_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&sbc_dec_sink_factory)); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&sbc_dec_src_factory)); + + gst_element_class_set_details(element_class, &sbc_dec_details); +} + +static void gst_sbc_dec_class_init(GstSbcDecClass *klass) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(klass); + + parent_class = g_type_class_peek_parent(klass); + + element_class->change_state = GST_DEBUG_FUNCPTR(sbc_dec_change_state); + + GST_DEBUG_CATEGORY_INIT(sbc_dec_debug, "sbcdec", 0, + "SBC decoding element"); +} + +static void gst_sbc_dec_init(GstSbcDec *self, GstSbcDecClass *klass) +{ + self->sinkpad = gst_pad_new_from_static_template( + &sbc_dec_sink_factory, "sink"); + gst_pad_set_chain_function(self->sinkpad, GST_DEBUG_FUNCPTR( + sbc_dec_chain)); + gst_element_add_pad(GST_ELEMENT(self), self->sinkpad); + + self->srcpad = gst_pad_new_from_static_template( + &sbc_dec_src_factory, "src"); + gst_element_add_pad(GST_ELEMENT(self), self->srcpad); + + self->outcaps = NULL; +} + +gboolean gst_sbc_dec_plugin_init(GstPlugin *plugin) +{ + return gst_element_register(plugin, "sbcdec", GST_RANK_PRIMARY, + GST_TYPE_SBC_DEC); +} diff --git a/audio/gstsbcdec.h b/audio/gstsbcdec.h new file mode 100644 index 0000000..c77feae --- /dev/null +++ b/audio/gstsbcdec.h @@ -0,0 +1,66 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include + +#include "sbc.h" + +G_BEGIN_DECLS + +#define GST_TYPE_SBC_DEC \ + (gst_sbc_dec_get_type()) +#define GST_SBC_DEC(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_SBC_DEC,GstSbcDec)) +#define GST_SBC_DEC_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_SBC_DEC,GstSbcDecClass)) +#define GST_IS_SBC_DEC(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_SBC_DEC)) +#define GST_IS_SBC_DEC_CLASS(obj) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_SBC_DEC)) + +typedef struct _GstSbcDec GstSbcDec; +typedef struct _GstSbcDecClass GstSbcDecClass; + +struct _GstSbcDec { + GstElement element; + + GstPad *sinkpad; + GstPad *srcpad; + + GstBuffer *buffer; + + /* caps for outgoing buffers */ + GstCaps *outcaps; + + sbc_t sbc; +}; + +struct _GstSbcDecClass { + GstElementClass parent_class; +}; + +GType gst_sbc_dec_get_type(void); + +gboolean gst_sbc_dec_plugin_init(GstPlugin *plugin); + +G_END_DECLS diff --git a/audio/gstsbcenc.c b/audio/gstsbcenc.c new file mode 100644 index 0000000..8948371 --- /dev/null +++ b/audio/gstsbcenc.c @@ -0,0 +1,601 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "gstpragma.h" +#include "gstsbcutil.h" +#include "gstsbcenc.h" + +#define SBC_ENC_DEFAULT_MODE SBC_MODE_AUTO +#define SBC_ENC_DEFAULT_BLOCKS 0 +#define SBC_ENC_DEFAULT_SUB_BANDS 0 +#define SBC_ENC_DEFAULT_ALLOCATION SBC_AM_AUTO +#define SBC_ENC_DEFAULT_RATE 0 +#define SBC_ENC_DEFAULT_CHANNELS 0 + +#define SBC_ENC_BITPOOL_AUTO 1 +#define SBC_ENC_BITPOOL_MIN 2 +#define SBC_ENC_BITPOOL_MIN_STR "2" +#define SBC_ENC_BITPOOL_MAX 64 +#define SBC_ENC_BITPOOL_MAX_STR "64" + +GST_DEBUG_CATEGORY_STATIC(sbc_enc_debug); +#define GST_CAT_DEFAULT sbc_enc_debug + +#define GST_TYPE_SBC_MODE (gst_sbc_mode_get_type()) + +static GType gst_sbc_mode_get_type(void) +{ + static GType sbc_mode_type = 0; + static GEnumValue sbc_modes[] = { + { SBC_MODE_MONO, "Mono", "mono" }, + { SBC_MODE_DUAL_CHANNEL, "Dual Channel", "dual" }, + { SBC_MODE_STEREO, "Stereo", "stereo"}, + { SBC_MODE_JOINT_STEREO, "Joint Stereo", "joint" }, + { SBC_MODE_AUTO, "Auto", "auto" }, + { -1, NULL, NULL} + }; + + if (!sbc_mode_type) + sbc_mode_type = g_enum_register_static("GstSbcMode", sbc_modes); + + return sbc_mode_type; +} + +#define GST_TYPE_SBC_ALLOCATION (gst_sbc_allocation_get_type()) + +static GType gst_sbc_allocation_get_type(void) +{ + static GType sbc_allocation_type = 0; + static GEnumValue sbc_allocations[] = { + { SBC_AM_LOUDNESS, "Loudness", "loudness" }, + { SBC_AM_SNR, "SNR", "snr" }, + { SBC_AM_AUTO, "Auto", "auto" }, + { -1, NULL, NULL} + }; + + if (!sbc_allocation_type) + sbc_allocation_type = g_enum_register_static( + "GstSbcAllocation", sbc_allocations); + + return sbc_allocation_type; +} + +#define GST_TYPE_SBC_BLOCKS (gst_sbc_blocks_get_type()) + +static GType gst_sbc_blocks_get_type(void) +{ + static GType sbc_blocks_type = 0; + static GEnumValue sbc_blocks[] = { + { 0, "Auto", "auto" }, + { 4, "4", "4" }, + { 8, "8", "8" }, + { 12, "12", "12" }, + { 16, "16", "16" }, + { -1, NULL, NULL} + }; + + if (!sbc_blocks_type) + sbc_blocks_type = g_enum_register_static( + "GstSbcBlocks", sbc_blocks); + + return sbc_blocks_type; +} + +#define GST_TYPE_SBC_SUBBANDS (gst_sbc_subbands_get_type()) + +static GType gst_sbc_subbands_get_type(void) +{ + static GType sbc_subbands_type = 0; + static GEnumValue sbc_subbands[] = { + { 0, "Auto", "auto" }, + { 4, "4 subbands", "4" }, + { 8, "8 subbands", "8" }, + { -1, NULL, NULL} + }; + + if (!sbc_subbands_type) + sbc_subbands_type = g_enum_register_static( + "GstSbcSubbands", sbc_subbands); + + return sbc_subbands_type; +} + +enum { + PROP_0, + PROP_MODE, + PROP_ALLOCATION, + PROP_BLOCKS, + PROP_SUBBANDS, + PROP_BITPOOL +}; + +GST_BOILERPLATE(GstSbcEnc, gst_sbc_enc, GstElement, GST_TYPE_ELEMENT); + +static const GstElementDetails sbc_enc_details = + GST_ELEMENT_DETAILS("Bluetooth SBC encoder", + "Codec/Encoder/Audio", + "Encode a SBC audio stream", + "Marcel Holtmann "); + +static GstStaticPadTemplate sbc_enc_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-raw-int, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "endianness = (int) BYTE_ORDER, " + "signed = (boolean) true, " + "width = (int) 16, " + "depth = (int) 16")); + +static GstStaticPadTemplate sbc_enc_src_factory = + GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "mode = (string) { \"mono\", \"dual\", \"stereo\", \"joint\" }, " + "blocks = (int) { 4, 8, 12, 16 }, " + "subbands = (int) { 4, 8 }, " + "allocation = (string) { \"snr\", \"loudness\" }, " + "bitpool = (int) [ " SBC_ENC_BITPOOL_MIN_STR + ", " SBC_ENC_BITPOOL_MAX_STR " ]")); + +gboolean gst_sbc_enc_fill_sbc_params(GstSbcEnc *enc, GstCaps *caps); + +static GstCaps *sbc_enc_generate_srcpad_caps(GstSbcEnc *enc) +{ + GstCaps *src_caps; + GstStructure *structure; + GEnumValue *enum_value; + GEnumClass *enum_class; + GValue *value; + + src_caps = gst_caps_copy(gst_pad_get_pad_template_caps(enc->srcpad)); + structure = gst_caps_get_structure(src_caps, 0); + + value = g_new0(GValue, 1); + + if (enc->rate != 0) + gst_sbc_util_set_structure_int_param(structure, "rate", + enc->rate, value); + + if (enc->channels != 0) + gst_sbc_util_set_structure_int_param(structure, "channels", + enc->channels, value); + + if (enc->subbands != 0) + gst_sbc_util_set_structure_int_param(structure, "subbands", + enc->subbands, value); + + if (enc->blocks != 0) + gst_sbc_util_set_structure_int_param(structure, "blocks", + enc->blocks, value); + + if (enc->bitpool != SBC_ENC_BITPOOL_AUTO) + gst_sbc_util_set_structure_int_param(structure, "bitpool", + enc->bitpool, value); + + if (enc->mode != SBC_ENC_DEFAULT_MODE) { + enum_class = g_type_class_ref(GST_TYPE_SBC_MODE); + enum_value = g_enum_get_value(enum_class, enc->mode); + gst_sbc_util_set_structure_string_param(structure, "mode", + enum_value->value_nick, value); + g_type_class_unref(enum_class); + } + + if (enc->allocation != SBC_AM_AUTO) { + enum_class = g_type_class_ref(GST_TYPE_SBC_ALLOCATION); + enum_value = g_enum_get_value(enum_class, enc->allocation); + gst_sbc_util_set_structure_string_param(structure, "allocation", + enum_value->value_nick, value); + g_type_class_unref(enum_class); + } + + g_free(value); + + return src_caps; +} + +static GstCaps *sbc_enc_src_getcaps(GstPad *pad) +{ + GstSbcEnc *enc; + + enc = GST_SBC_ENC(GST_PAD_PARENT(pad)); + + return sbc_enc_generate_srcpad_caps(enc); +} + +static gboolean sbc_enc_src_setcaps(GstPad *pad, GstCaps *caps) +{ + GstSbcEnc *enc = GST_SBC_ENC(GST_PAD_PARENT(pad)); + + GST_LOG_OBJECT(enc, "setting srcpad caps"); + + return gst_sbc_enc_fill_sbc_params(enc, caps); +} + +static GstCaps *sbc_enc_src_caps_fixate(GstSbcEnc *enc, GstCaps *caps) +{ + gchar *error_message = NULL; + GstCaps *result; + + result = gst_sbc_util_caps_fixate(caps, &error_message); + + if (!result) { + GST_WARNING_OBJECT(enc, "Invalid input caps caused parsing " + "error: %s", error_message); + g_free(error_message); + return NULL; + } + + return result; +} + +static GstCaps *sbc_enc_get_fixed_srcpad_caps(GstSbcEnc *enc) +{ + GstCaps *caps; + gboolean res = TRUE; + GstCaps *result_caps = NULL; + + caps = gst_pad_get_allowed_caps(enc->srcpad); + if (caps == NULL) + caps = sbc_enc_src_getcaps(enc->srcpad); + + if (caps == GST_CAPS_NONE || gst_caps_is_empty(caps)) { + res = FALSE; + goto done; + } + + result_caps = sbc_enc_src_caps_fixate(enc, caps); + +done: + gst_caps_unref(caps); + + if (!res) + return NULL; + + return result_caps; +} + +static gboolean sbc_enc_sink_setcaps(GstPad *pad, GstCaps *caps) +{ + GstSbcEnc *enc; + GstStructure *structure; + GstCaps *src_caps; + gint rate, channels; + gboolean res; + + enc = GST_SBC_ENC(GST_PAD_PARENT(pad)); + structure = gst_caps_get_structure(caps, 0); + + if (!gst_structure_get_int(structure, "rate", &rate)) + return FALSE; + if (!gst_structure_get_int(structure, "channels", &channels)) + return FALSE; + + enc->rate = rate; + enc->channels = channels; + + src_caps = sbc_enc_get_fixed_srcpad_caps(enc); + if (!src_caps) + return FALSE; + res = gst_pad_set_caps(enc->srcpad, src_caps); + gst_caps_unref(src_caps); + + return res; +} + +gboolean gst_sbc_enc_fill_sbc_params(GstSbcEnc *enc, GstCaps *caps) +{ + if (!gst_caps_is_fixed(caps)) { + GST_DEBUG_OBJECT(enc, "didn't receive fixed caps, " + "returning false"); + return FALSE; + } + + if (!gst_sbc_util_fill_sbc_params(&enc->sbc, caps)) + return FALSE; + + if (enc->rate != 0 && gst_sbc_parse_rate_from_sbc(enc->sbc.frequency) + != enc->rate) + goto fail; + + if (enc->channels != 0 && gst_sbc_get_channel_number(enc->sbc.mode) + != enc->channels) + goto fail; + + if (enc->blocks != 0 && gst_sbc_parse_blocks_from_sbc(enc->sbc.blocks) + != enc->blocks) + goto fail; + + if (enc->subbands != 0 && gst_sbc_parse_subbands_from_sbc( + enc->sbc.subbands) != enc->subbands) + goto fail; + + if (enc->mode != SBC_ENC_DEFAULT_MODE && enc->sbc.mode != enc->mode) + goto fail; + + if (enc->allocation != SBC_AM_AUTO && + enc->sbc.allocation != enc->allocation) + goto fail; + + if (enc->bitpool != SBC_ENC_BITPOOL_AUTO && + enc->sbc.bitpool != enc->bitpool) + goto fail; + + enc->codesize = sbc_get_codesize(&enc->sbc); + enc->frame_length = sbc_get_frame_length(&enc->sbc); + enc->frame_duration = sbc_get_frame_duration(&enc->sbc); + + GST_DEBUG_OBJECT(enc, "codesize: %d, frame_length: %d, frame_duration:" + " %d", enc->codesize, enc->frame_length, + enc->frame_duration); + + return TRUE; + +fail: + memset(&enc->sbc, 0, sizeof(sbc_t)); + return FALSE; +} + +static GstFlowReturn sbc_enc_chain(GstPad *pad, GstBuffer *buffer) +{ + GstSbcEnc *enc = GST_SBC_ENC(gst_pad_get_parent(pad)); + GstAdapter *adapter = enc->adapter; + GstFlowReturn res = GST_FLOW_OK; + + gst_adapter_push(adapter, buffer); + + while (gst_adapter_available(adapter) >= enc->codesize && + res == GST_FLOW_OK) { + GstBuffer *output; + GstCaps *caps; + const guint8 *data; + gint consumed; + + caps = GST_PAD_CAPS(enc->srcpad); + res = gst_pad_alloc_buffer_and_set_caps(enc->srcpad, + GST_BUFFER_OFFSET_NONE, + enc->frame_length, caps, + &output); + if (res != GST_FLOW_OK) + goto done; + + data = gst_adapter_peek(adapter, enc->codesize); + + consumed = sbc_encode(&enc->sbc, (gpointer) data, + enc->codesize, + GST_BUFFER_DATA(output), + GST_BUFFER_SIZE(output), NULL); + if (consumed <= 0) { + GST_DEBUG_OBJECT(enc, "comsumed < 0, codesize: %d", + enc->codesize); + break; + } + gst_adapter_flush(adapter, consumed); + + GST_BUFFER_TIMESTAMP(output) = GST_BUFFER_TIMESTAMP(buffer); + /* we have only 1 frame */ + GST_BUFFER_DURATION(output) = enc->frame_duration; + + res = gst_pad_push(enc->srcpad, output); + + if (res != GST_FLOW_OK) + goto done; + } + +done: + gst_object_unref(enc); + + return res; +} + +static GstStateChangeReturn sbc_enc_change_state(GstElement *element, + GstStateChange transition) +{ + GstSbcEnc *enc = GST_SBC_ENC(element); + + switch (transition) { + case GST_STATE_CHANGE_READY_TO_PAUSED: + GST_DEBUG("Setup subband codec"); + sbc_init(&enc->sbc, 0); + break; + + case GST_STATE_CHANGE_PAUSED_TO_READY: + GST_DEBUG("Finish subband codec"); + sbc_finish(&enc->sbc); + break; + + default: + break; + } + + return parent_class->change_state(element, transition); +} + +static void gst_sbc_enc_dispose(GObject *object) +{ + GstSbcEnc *enc = GST_SBC_ENC(object); + + if (enc->adapter != NULL) + g_object_unref(G_OBJECT(enc->adapter)); + + enc->adapter = NULL; +} + +static void gst_sbc_enc_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&sbc_enc_sink_factory)); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&sbc_enc_src_factory)); + + gst_element_class_set_details(element_class, &sbc_enc_details); +} + +static void gst_sbc_enc_set_property(GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + GstSbcEnc *enc = GST_SBC_ENC(object); + + /* changes to those properties will only happen on the next caps + * negotiation */ + + switch (prop_id) { + case PROP_MODE: + enc->mode = g_value_get_enum(value); + break; + case PROP_ALLOCATION: + enc->allocation = g_value_get_enum(value); + break; + case PROP_BLOCKS: + enc->blocks = g_value_get_enum(value); + break; + case PROP_SUBBANDS: + enc->subbands = g_value_get_enum(value); + break; + case PROP_BITPOOL: + enc->bitpool = g_value_get_int(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_sbc_enc_get_property(GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GstSbcEnc *enc = GST_SBC_ENC(object); + + switch (prop_id) { + case PROP_MODE: + g_value_set_enum(value, enc->mode); + break; + case PROP_ALLOCATION: + g_value_set_enum(value, enc->allocation); + break; + case PROP_BLOCKS: + g_value_set_enum(value, enc->blocks); + break; + case PROP_SUBBANDS: + g_value_set_enum(value, enc->subbands); + break; + case PROP_BITPOOL: + g_value_set_int(value, enc->bitpool); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_sbc_enc_class_init(GstSbcEncClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GstElementClass *element_class = GST_ELEMENT_CLASS(klass); + + parent_class = g_type_class_peek_parent(klass); + + object_class->set_property = GST_DEBUG_FUNCPTR(gst_sbc_enc_set_property); + object_class->get_property = GST_DEBUG_FUNCPTR(gst_sbc_enc_get_property); + object_class->dispose = GST_DEBUG_FUNCPTR(gst_sbc_enc_dispose); + + element_class->change_state = GST_DEBUG_FUNCPTR(sbc_enc_change_state); + + g_object_class_install_property(object_class, PROP_MODE, + g_param_spec_enum("mode", "Mode", + "Encoding mode", GST_TYPE_SBC_MODE, + SBC_ENC_DEFAULT_MODE, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_ALLOCATION, + g_param_spec_enum("allocation", "Allocation", + "Allocation method", GST_TYPE_SBC_ALLOCATION, + SBC_ENC_DEFAULT_ALLOCATION, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_BLOCKS, + g_param_spec_enum("blocks", "Blocks", + "Blocks", GST_TYPE_SBC_BLOCKS, + SBC_ENC_DEFAULT_BLOCKS, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_SUBBANDS, + g_param_spec_enum("subbands", "Sub bands", + "Number of sub bands", GST_TYPE_SBC_SUBBANDS, + SBC_ENC_DEFAULT_SUB_BANDS, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_BITPOOL, + g_param_spec_int("bitpool", "Bitpool", + "Bitpool (use 1 for automatic selection)", + SBC_ENC_BITPOOL_AUTO, SBC_ENC_BITPOOL_MAX, + SBC_ENC_BITPOOL_AUTO, G_PARAM_READWRITE)); + + GST_DEBUG_CATEGORY_INIT(sbc_enc_debug, "sbcenc", 0, + "SBC encoding element"); +} + +static void gst_sbc_enc_init(GstSbcEnc *self, GstSbcEncClass *klass) +{ + self->sinkpad = gst_pad_new_from_static_template( + &sbc_enc_sink_factory, "sink"); + gst_pad_set_setcaps_function(self->sinkpad, + GST_DEBUG_FUNCPTR(sbc_enc_sink_setcaps)); + gst_element_add_pad(GST_ELEMENT(self), self->sinkpad); + + self->srcpad = gst_pad_new_from_static_template( + &sbc_enc_src_factory, "src"); + gst_pad_set_getcaps_function(self->srcpad, + GST_DEBUG_FUNCPTR(sbc_enc_src_getcaps)); + gst_pad_set_setcaps_function(self->srcpad, + GST_DEBUG_FUNCPTR(sbc_enc_src_setcaps)); + gst_element_add_pad(GST_ELEMENT(self), self->srcpad); + + gst_pad_set_chain_function(self->sinkpad, + GST_DEBUG_FUNCPTR(sbc_enc_chain)); + + self->subbands = SBC_ENC_DEFAULT_SUB_BANDS; + self->blocks = SBC_ENC_DEFAULT_BLOCKS; + self->mode = SBC_ENC_DEFAULT_MODE; + self->allocation = SBC_ENC_DEFAULT_ALLOCATION; + self->rate = SBC_ENC_DEFAULT_RATE; + self->channels = SBC_ENC_DEFAULT_CHANNELS; + self->bitpool = SBC_ENC_BITPOOL_AUTO; + + self->frame_length = 0; + self->frame_duration = 0; + + self->adapter = gst_adapter_new(); +} + +gboolean gst_sbc_enc_plugin_init(GstPlugin *plugin) +{ + return gst_element_register(plugin, "sbcenc", + GST_RANK_NONE, GST_TYPE_SBC_ENC); +} diff --git a/audio/gstsbcenc.h b/audio/gstsbcenc.h new file mode 100644 index 0000000..0329351 --- /dev/null +++ b/audio/gstsbcenc.h @@ -0,0 +1,75 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include + +#include "sbc.h" + +G_BEGIN_DECLS + +#define GST_TYPE_SBC_ENC \ + (gst_sbc_enc_get_type()) +#define GST_SBC_ENC(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_SBC_ENC,GstSbcEnc)) +#define GST_SBC_ENC_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_SBC_ENC,GstSbcEncClass)) +#define GST_IS_SBC_ENC(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_SBC_ENC)) +#define GST_IS_SBC_ENC_CLASS(obj) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_SBC_ENC)) + +typedef struct _GstSbcEnc GstSbcEnc; +typedef struct _GstSbcEncClass GstSbcEncClass; + +struct _GstSbcEnc { + GstElement element; + + GstPad *sinkpad; + GstPad *srcpad; + GstAdapter *adapter; + + gint rate; + gint channels; + gint mode; + gint blocks; + gint allocation; + gint subbands; + gint bitpool; + + guint codesize; + gint frame_length; + gint frame_duration; + + sbc_t sbc; +}; + +struct _GstSbcEncClass { + GstElementClass parent_class; +}; + +GType gst_sbc_enc_get_type(void); + +gboolean gst_sbc_enc_plugin_init(GstPlugin *plugin); + +G_END_DECLS diff --git a/audio/gstsbcparse.c b/audio/gstsbcparse.c new file mode 100644 index 0000000..bc4c0c7 --- /dev/null +++ b/audio/gstsbcparse.c @@ -0,0 +1,219 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "gstpragma.h" +#include "gstsbcutil.h" +#include "gstsbcparse.h" + +GST_DEBUG_CATEGORY_STATIC(sbc_parse_debug); +#define GST_CAT_DEFAULT sbc_parse_debug + +GST_BOILERPLATE(GstSbcParse, gst_sbc_parse, GstElement, GST_TYPE_ELEMENT); + +static const GstElementDetails sbc_parse_details = + GST_ELEMENT_DETAILS("Bluetooth SBC parser", + "Codec/Parser/Audio", + "Parse a SBC audio stream", + "Marcel Holtmann "); + +static GstStaticPadTemplate sbc_parse_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc," + "parsed = (boolean) false")); + +static GstStaticPadTemplate sbc_parse_src_factory = + GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "mode = (string) { \"mono\", \"dual\", \"stereo\", \"joint\" }, " + "blocks = (int) { 4, 8, 12, 16 }, " + "subbands = (int) { 4, 8 }, " + "allocation = (string) { \"snr\", \"loudness\" }," + "bitpool = (int) [ 2, 64 ]," + "parsed = (boolean) true")); + +static GstFlowReturn sbc_parse_chain(GstPad *pad, GstBuffer *buffer) +{ + GstSbcParse *parse = GST_SBC_PARSE(gst_pad_get_parent(pad)); + GstFlowReturn res = GST_FLOW_OK; + guint size, offset = 0; + guint8 *data; + + /* FIXME use a gstadpter */ + if (parse->buffer) { + GstBuffer *temp; + temp = buffer; + buffer = gst_buffer_span(parse->buffer, 0, buffer, + GST_BUFFER_SIZE(parse->buffer) + + GST_BUFFER_SIZE(buffer)); + gst_buffer_unref(parse->buffer); + gst_buffer_unref(temp); + parse->buffer = NULL; + } + + data = GST_BUFFER_DATA(buffer); + size = GST_BUFFER_SIZE(buffer); + + while (offset < size) { + GstBuffer *output; + int consumed; + + consumed = sbc_parse(&parse->new_sbc, data + offset, + size - offset); + if (consumed <= 0) + break; + + if (parse->first_parsing || (memcmp(&parse->sbc, + &parse->new_sbc, sizeof(sbc_t)) != 0)) { + + memcpy(&parse->sbc, &parse->new_sbc, sizeof(sbc_t)); + if (parse->outcaps != NULL) + gst_caps_unref(parse->outcaps); + + parse->outcaps = gst_sbc_parse_caps_from_sbc( + &parse->sbc); + + parse->first_parsing = FALSE; + } + + res = gst_pad_alloc_buffer_and_set_caps(parse->srcpad, + GST_BUFFER_OFFSET_NONE, + consumed, parse->outcaps, &output); + + if (res != GST_FLOW_OK) + goto done; + + memcpy(GST_BUFFER_DATA(output), data + offset, consumed); + + res = gst_pad_push(parse->srcpad, output); + if (res != GST_FLOW_OK) + goto done; + + offset += consumed; + } + + if (offset < size) + parse->buffer = gst_buffer_create_sub(buffer, + offset, size - offset); + +done: + gst_buffer_unref(buffer); + gst_object_unref(parse); + + return res; +} + +static GstStateChangeReturn sbc_parse_change_state(GstElement *element, + GstStateChange transition) +{ + GstSbcParse *parse = GST_SBC_PARSE(element); + + switch (transition) { + case GST_STATE_CHANGE_READY_TO_PAUSED: + GST_DEBUG("Setup subband codec"); + + parse->channels = -1; + parse->rate = -1; + parse->first_parsing = TRUE; + + sbc_init(&parse->sbc, 0); + break; + + case GST_STATE_CHANGE_PAUSED_TO_READY: + GST_DEBUG("Finish subband codec"); + + if (parse->buffer) { + gst_buffer_unref(parse->buffer); + parse->buffer = NULL; + } + if (parse->outcaps != NULL) { + gst_caps_unref(parse->outcaps); + parse->outcaps = NULL; + } + + sbc_finish(&parse->sbc); + break; + + default: + break; + } + + return parent_class->change_state(element, transition); +} + +static void gst_sbc_parse_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&sbc_parse_sink_factory)); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&sbc_parse_src_factory)); + + gst_element_class_set_details(element_class, &sbc_parse_details); +} + +static void gst_sbc_parse_class_init(GstSbcParseClass *klass) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(klass); + + parent_class = g_type_class_peek_parent(klass); + + element_class->change_state = GST_DEBUG_FUNCPTR(sbc_parse_change_state); + + GST_DEBUG_CATEGORY_INIT(sbc_parse_debug, "sbcparse", 0, + "SBC parsing element"); +} + +static void gst_sbc_parse_init(GstSbcParse *self, GstSbcParseClass *klass) +{ + self->sinkpad = gst_pad_new_from_static_template( + &sbc_parse_sink_factory, "sink"); + gst_pad_set_chain_function(self->sinkpad, + GST_DEBUG_FUNCPTR(sbc_parse_chain)); + gst_element_add_pad(GST_ELEMENT(self), self->sinkpad); + + self->srcpad = gst_pad_new_from_static_template( + &sbc_parse_src_factory, "src"); + gst_element_add_pad(GST_ELEMENT(self), self->srcpad); + + self->outcaps = NULL; + self->buffer = NULL; + self->channels = -1; + self->rate = -1; + self->first_parsing = TRUE; +} + +gboolean gst_sbc_parse_plugin_init(GstPlugin *plugin) +{ + return gst_element_register(plugin, "sbcparse", GST_RANK_NONE, + GST_TYPE_SBC_PARSE); +} diff --git a/audio/gstsbcparse.h b/audio/gstsbcparse.h new file mode 100644 index 0000000..ecb8be4 --- /dev/null +++ b/audio/gstsbcparse.h @@ -0,0 +1,69 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include + +#include "sbc.h" + +G_BEGIN_DECLS + +#define GST_TYPE_SBC_PARSE \ + (gst_sbc_parse_get_type()) +#define GST_SBC_PARSE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_SBC_PARSE,GstSbcParse)) +#define GST_SBC_PARSE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_SBC_PARSE,GstSbcParseClass)) +#define GST_IS_SBC_PARSE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_SBC_PARSE)) +#define GST_IS_SBC_PARSE_CLASS(obj) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_SBC_PARSE)) + +typedef struct _GstSbcParse GstSbcParse; +typedef struct _GstSbcParseClass GstSbcParseClass; + +struct _GstSbcParse { + GstElement element; + + GstPad *sinkpad; + GstPad *srcpad; + + GstBuffer *buffer; + + sbc_t sbc; + sbc_t new_sbc; + GstCaps *outcaps; + gboolean first_parsing; + + gint channels; + gint rate; +}; + +struct _GstSbcParseClass { + GstElementClass parent_class; +}; + +GType gst_sbc_parse_get_type(void); + +gboolean gst_sbc_parse_plugin_init(GstPlugin *plugin); + +G_END_DECLS diff --git a/audio/gstsbcutil.c b/audio/gstsbcutil.c new file mode 100644 index 0000000..f596b41 --- /dev/null +++ b/audio/gstsbcutil.c @@ -0,0 +1,520 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include "gstsbcutil.h" + +/* + * Selects one rate from a list of possible rates + * TODO - use a better approach to this (it is selecting the last element) + */ +gint gst_sbc_select_rate_from_list(const GValue *value) +{ + guint size = gst_value_list_get_size(value); + return g_value_get_int(gst_value_list_get_value(value, size-1)); +} + +/* + * Selects one number of channels option from a range of possible numbers + * TODO - use a better approach to this (it is selecting the maximum value) + */ +gint gst_sbc_select_channels_from_range(const GValue *value) +{ + return gst_value_get_int_range_max(value); +} + +/* + * Selects one number of blocks from a list of possible blocks + * TODO - use a better approach to this (it is selecting the last element) + */ +gint gst_sbc_select_blocks_from_list(const GValue *value) +{ + guint size = gst_value_list_get_size(value); + return g_value_get_int(gst_value_list_get_value(value, size-1)); +} + +/* + * Selects one number of subbands from a list + * TODO - use a better approach to this (it is selecting the last element) + */ +gint gst_sbc_select_subbands_from_list(const GValue *value) +{ + guint size = gst_value_list_get_size(value); + return g_value_get_int(gst_value_list_get_value(value, size-1)); +} + +/* + * Selects one bitpool option from a range + * TODO - use a better approach to this (it is selecting the maximum value) + */ +gint gst_sbc_select_bitpool_from_range(const GValue *value) +{ + return gst_value_get_int_range_max(value); +} + +/* + * Selects one allocation mode from the ones on the list + * TODO - use a better approach + */ +const gchar *gst_sbc_get_allocation_from_list(const GValue *value) +{ + guint size = gst_value_list_get_size(value); + return g_value_get_string(gst_value_list_get_value(value, size-1)); +} + +/* + * Selects one mode from the ones on the list + */ +const gchar *gst_sbc_get_mode_from_list(const GValue *list, gint channels) +{ + unsigned int i; + const GValue *value; + const gchar *aux; + gboolean joint, stereo, dual, mono; + guint size = gst_value_list_get_size(list); + + joint = stereo = dual = mono = FALSE; + + for (i = 0; i < size; i++) { + value = gst_value_list_get_value(list, i); + aux = g_value_get_string(value); + if (strcmp("joint", aux) == 0) + joint = TRUE; + else if (strcmp("stereo", aux) == 0) + stereo = TRUE; + else if (strcmp("dual", aux) == 0) + dual = TRUE; + else if (strcmp("mono", aux) == 0) + mono = TRUE; + } + + if (channels == 1 && mono) + return "mono"; + else if (channels == 2) { + if (joint) + return "joint"; + else if (stereo) + return "stereo"; + else if (dual) + return "dual"; + } + + return NULL; +} + +gint gst_sbc_parse_rate_from_sbc(gint frequency) +{ + switch (frequency) { + case SBC_FREQ_16000: + return 16000; + case SBC_FREQ_32000: + return 32000; + case SBC_FREQ_44100: + return 44100; + case SBC_FREQ_48000: + return 48000; + default: + return 0; + } +} + +gint gst_sbc_parse_rate_to_sbc(gint rate) +{ + switch (rate) { + case 16000: + return SBC_FREQ_16000; + case 32000: + return SBC_FREQ_32000; + case 44100: + return SBC_FREQ_44100; + case 48000: + return SBC_FREQ_48000; + default: + return -1; + } +} + +gint gst_sbc_get_channel_number(gint mode) +{ + switch (mode) { + case SBC_MODE_JOINT_STEREO: + case SBC_MODE_STEREO: + case SBC_MODE_DUAL_CHANNEL: + return 2; + case SBC_MODE_MONO: + return 1; + default: + return 0; + } +} + +gint gst_sbc_parse_subbands_from_sbc(gint subbands) +{ + switch (subbands) { + case SBC_SB_4: + return 4; + case SBC_SB_8: + return 8; + default: + return 0; + } +} + +gint gst_sbc_parse_subbands_to_sbc(gint subbands) +{ + switch (subbands) { + case 4: + return SBC_SB_4; + case 8: + return SBC_SB_8; + default: + return -1; + } +} + +gint gst_sbc_parse_blocks_from_sbc(gint blocks) +{ + switch (blocks) { + case SBC_BLK_4: + return 4; + case SBC_BLK_8: + return 8; + case SBC_BLK_12: + return 12; + case SBC_BLK_16: + return 16; + default: + return 0; + } +} + +gint gst_sbc_parse_blocks_to_sbc(gint blocks) +{ + switch (blocks) { + case 4: + return SBC_BLK_4; + case 8: + return SBC_BLK_8; + case 12: + return SBC_BLK_12; + case 16: + return SBC_BLK_16; + default: + return -1; + } +} + +const gchar *gst_sbc_parse_mode_from_sbc(gint mode) +{ + switch (mode) { + case SBC_MODE_MONO: + return "mono"; + case SBC_MODE_DUAL_CHANNEL: + return "dual"; + case SBC_MODE_STEREO: + return "stereo"; + case SBC_MODE_JOINT_STEREO: + case SBC_MODE_AUTO: + return "joint"; + default: + return NULL; + } +} + +gint gst_sbc_parse_mode_to_sbc(const gchar *mode) +{ + if (g_ascii_strcasecmp(mode, "joint") == 0) + return SBC_MODE_JOINT_STEREO; + else if (g_ascii_strcasecmp(mode, "stereo") == 0) + return SBC_MODE_STEREO; + else if (g_ascii_strcasecmp(mode, "dual") == 0) + return SBC_MODE_DUAL_CHANNEL; + else if (g_ascii_strcasecmp(mode, "mono") == 0) + return SBC_MODE_MONO; + else if (g_ascii_strcasecmp(mode, "auto") == 0) + return SBC_MODE_JOINT_STEREO; + else + return -1; +} + +const gchar *gst_sbc_parse_allocation_from_sbc(gint alloc) +{ + switch (alloc) { + case SBC_AM_LOUDNESS: + return "loudness"; + case SBC_AM_SNR: + return "snr"; + case SBC_AM_AUTO: + return "loudness"; + default: + return NULL; + } +} + +gint gst_sbc_parse_allocation_to_sbc(const gchar *allocation) +{ + if (g_ascii_strcasecmp(allocation, "loudness") == 0) + return SBC_AM_LOUDNESS; + else if (g_ascii_strcasecmp(allocation, "snr") == 0) + return SBC_AM_SNR; + else + return SBC_AM_LOUDNESS; +} + +GstCaps *gst_sbc_parse_caps_from_sbc(sbc_t *sbc) +{ + GstCaps *caps; + const gchar *mode_str; + const gchar *allocation_str; + + mode_str = gst_sbc_parse_mode_from_sbc(sbc->mode); + allocation_str = gst_sbc_parse_allocation_from_sbc(sbc->allocation); + caps = gst_caps_new_simple("audio/x-sbc", + "rate", G_TYPE_INT, + gst_sbc_parse_rate_from_sbc(sbc->frequency), + "channels", G_TYPE_INT, + gst_sbc_get_channel_number(sbc->mode), + "mode", G_TYPE_STRING, mode_str, + "subbands", G_TYPE_INT, + gst_sbc_parse_subbands_from_sbc(sbc->subbands), + "blocks", G_TYPE_INT, + gst_sbc_parse_blocks_from_sbc(sbc->blocks), + "allocation", G_TYPE_STRING, allocation_str, + "bitpool", G_TYPE_INT, sbc->bitpool, + NULL); + + return caps; +} + +/* + * Given a GstCaps, this will return a fixed GstCaps on successful conversion. + * If an error occurs, it will return NULL and error_message will contain the + * error message. + * + * error_message must be passed NULL, if an error occurs, the caller has the + * ownership of the error_message, it must be freed after use. + */ +GstCaps *gst_sbc_util_caps_fixate(GstCaps *caps, gchar **error_message) +{ + GstCaps *result; + GstStructure *structure; + const GValue *value; + gboolean error = FALSE; + gint temp, rate, channels, blocks, subbands, bitpool; + const gchar *allocation = NULL; + const gchar *mode = NULL; + + g_assert(*error_message == NULL); + + structure = gst_caps_get_structure(caps, 0); + + if (!gst_structure_has_field(structure, "rate")) { + error = TRUE; + *error_message = g_strdup("no rate"); + goto error; + } else { + value = gst_structure_get_value(structure, "rate"); + if (GST_VALUE_HOLDS_LIST(value)) + temp = gst_sbc_select_rate_from_list(value); + else + temp = g_value_get_int(value); + rate = temp; + } + + if (!gst_structure_has_field(structure, "channels")) { + error = TRUE; + *error_message = g_strdup("no channels"); + goto error; + } else { + value = gst_structure_get_value(structure, "channels"); + if (GST_VALUE_HOLDS_INT_RANGE(value)) + temp = gst_sbc_select_channels_from_range(value); + else + temp = g_value_get_int(value); + channels = temp; + } + + if (!gst_structure_has_field(structure, "blocks")) { + error = TRUE; + *error_message = g_strdup("no blocks."); + goto error; + } else { + value = gst_structure_get_value(structure, "blocks"); + if (GST_VALUE_HOLDS_LIST(value)) + temp = gst_sbc_select_blocks_from_list(value); + else + temp = g_value_get_int(value); + blocks = temp; + } + + if (!gst_structure_has_field(structure, "subbands")) { + error = TRUE; + *error_message = g_strdup("no subbands"); + goto error; + } else { + value = gst_structure_get_value(structure, "subbands"); + if (GST_VALUE_HOLDS_LIST(value)) + temp = gst_sbc_select_subbands_from_list(value); + else + temp = g_value_get_int(value); + subbands = temp; + } + + if (!gst_structure_has_field(structure, "bitpool")) { + error = TRUE; + *error_message = g_strdup("no bitpool"); + goto error; + } else { + value = gst_structure_get_value(structure, "bitpool"); + if (GST_VALUE_HOLDS_INT_RANGE(value)) + temp = gst_sbc_select_bitpool_from_range(value); + else + temp = g_value_get_int(value); + bitpool = temp; + } + + if (!gst_structure_has_field(structure, "allocation")) { + error = TRUE; + *error_message = g_strdup("no allocation"); + goto error; + } else { + value = gst_structure_get_value(structure, "allocation"); + if (GST_VALUE_HOLDS_LIST(value)) + allocation = gst_sbc_get_allocation_from_list(value); + else + allocation = g_value_get_string(value); + } + + if (!gst_structure_has_field(structure, "mode")) { + error = TRUE; + *error_message = g_strdup("no mode"); + goto error; + } else { + value = gst_structure_get_value(structure, "mode"); + if (GST_VALUE_HOLDS_LIST(value)) { + mode = gst_sbc_get_mode_from_list(value, channels); + } else + mode = g_value_get_string(value); + } + + /* perform validation + * if channels is 1, we must have channel mode = mono + * if channels is 2, we can't have channel mode = mono */ + if ( (channels == 1 && (strcmp(mode, "mono") != 0) ) || + ( channels == 2 && ( strcmp(mode, "mono") == 0))) { + *error_message = g_strdup_printf("Invalid combination of " + "channels (%d) and channel mode (%s)", + channels, mode); + error = TRUE; + } + +error: + if (error) + return NULL; + + result = gst_caps_new_simple("audio/x-sbc", + "rate", G_TYPE_INT, rate, + "channels", G_TYPE_INT, channels, + "mode", G_TYPE_STRING, mode, + "blocks", G_TYPE_INT, blocks, + "subbands", G_TYPE_INT, subbands, + "allocation", G_TYPE_STRING, allocation, + "bitpool", G_TYPE_INT, bitpool, + NULL); + + return result; +} + +/** + * Sets the int field_value to the param "field" on the structure. + * value is used to do the operation, it must be a uninitialized (zero-filled) + * GValue, it will be left unitialized at the end of the function. + */ +void gst_sbc_util_set_structure_int_param(GstStructure *structure, + const gchar *field, gint field_value, + GValue *value) +{ + value = g_value_init(value, G_TYPE_INT); + g_value_set_int(value, field_value); + gst_structure_set_value(structure, field, value); + g_value_unset(value); +} + +/** + * Sets the string field_value to the param "field" on the structure. + * value is used to do the operation, it must be a uninitialized (zero-filled) + * GValue, it will be left unitialized at the end of the function. + */ +void gst_sbc_util_set_structure_string_param(GstStructure *structure, + const gchar *field, const gchar *field_value, + GValue *value) +{ + value = g_value_init(value, G_TYPE_STRING); + g_value_set_string(value, field_value); + gst_structure_set_value(structure, field, value); + g_value_unset(value); +} + +gboolean gst_sbc_util_fill_sbc_params(sbc_t *sbc, GstCaps *caps) +{ + GstStructure *structure; + gint rate, channels, subbands, blocks, bitpool; + const gchar *mode; + const gchar *allocation; + + g_assert(gst_caps_is_fixed(caps)); + + structure = gst_caps_get_structure(caps, 0); + + if (!gst_structure_get_int(structure, "rate", &rate)) + return FALSE; + if (!gst_structure_get_int(structure, "channels", &channels)) + return FALSE; + if (!gst_structure_get_int(structure, "subbands", &subbands)) + return FALSE; + if (!gst_structure_get_int(structure, "blocks", &blocks)) + return FALSE; + if (!gst_structure_get_int(structure, "bitpool", &bitpool)) + return FALSE; + + if (!(mode = gst_structure_get_string(structure, "mode"))) + return FALSE; + if (!(allocation = gst_structure_get_string(structure, "allocation"))) + return FALSE; + + if (channels == 1 && strcmp(mode, "mono") != 0) + return FALSE; + + sbc->frequency = gst_sbc_parse_rate_to_sbc(rate); + sbc->blocks = gst_sbc_parse_blocks_to_sbc(blocks); + sbc->subbands = gst_sbc_parse_subbands_to_sbc(subbands); + sbc->bitpool = bitpool; + sbc->mode = gst_sbc_parse_mode_to_sbc(mode); + sbc->allocation = gst_sbc_parse_allocation_to_sbc(allocation); + + return TRUE; +} diff --git a/audio/gstsbcutil.h b/audio/gstsbcutil.h new file mode 100644 index 0000000..5e47119 --- /dev/null +++ b/audio/gstsbcutil.h @@ -0,0 +1,74 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include + +#include "sbc.h" +#include + +#define SBC_AM_AUTO 0x02 +#define SBC_MODE_AUTO 0x04 + +gint gst_sbc_select_rate_from_list(const GValue *value); + +gint gst_sbc_select_channels_from_range(const GValue *value); + +gint gst_sbc_select_blocks_from_list(const GValue *value); + +gint gst_sbc_select_subbands_from_list(const GValue *value); + +gint gst_sbc_select_bitpool_from_range(const GValue *value); + +const gchar *gst_sbc_get_allocation_from_list(const GValue *value); + +const gchar *gst_sbc_get_mode_from_list(const GValue *value, gint channels); + +gint gst_sbc_get_channel_number(gint mode); +gint gst_sbc_parse_rate_from_sbc(gint frequency); +gint gst_sbc_parse_rate_to_sbc(gint rate); + +gint gst_sbc_parse_subbands_from_sbc(gint subbands); +gint gst_sbc_parse_subbands_to_sbc(gint subbands); + +gint gst_sbc_parse_blocks_from_sbc(gint blocks); +gint gst_sbc_parse_blocks_to_sbc(gint blocks); + +const gchar *gst_sbc_parse_mode_from_sbc(gint mode); +gint gst_sbc_parse_mode_to_sbc(const gchar *mode); + +const gchar *gst_sbc_parse_allocation_from_sbc(gint alloc); +gint gst_sbc_parse_allocation_to_sbc(const gchar *allocation); + +GstCaps* gst_sbc_parse_caps_from_sbc(sbc_t *sbc); + +GstCaps* gst_sbc_util_caps_fixate(GstCaps *caps, gchar** error_message); + +void gst_sbc_util_set_structure_int_param(GstStructure *structure, + const gchar* field, gint field_value, + GValue *value); + +void gst_sbc_util_set_structure_string_param(GstStructure *structure, + const gchar* field, const gchar* field_value, + GValue *value); + +gboolean gst_sbc_util_fill_sbc_params(sbc_t *sbc, GstCaps *caps); diff --git a/audio/headset.c b/audio/headset.c new file mode 100644 index 0000000..2bb1b4f --- /dev/null +++ b/audio/headset.c @@ -0,0 +1,3471 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "log.h" +#include "device.h" +#include "manager.h" +#include "error.h" +#include "telephony.h" +#include "headset.h" +#include "sdp-client.h" +#include "btio.h" +#include "dbus-common.h" +#include "../src/adapter.h" +#include "../src/device.h" + +#define DC_TIMEOUT 3 + +#define RING_INTERVAL 3 + +#define BUF_SIZE 1024 + +#define HEADSET_GAIN_SPEAKER 'S' +#define HEADSET_GAIN_MICROPHONE 'M' + +static struct { + gboolean telephony_ready; /* Telephony plugin initialized */ + uint32_t features; /* HFP AG features */ + const struct indicator *indicators; /* Available HFP indicators */ + int er_mode; /* Event reporting mode */ + int er_ind; /* Event reporting for indicators */ + int rh; /* Response and Hold state */ + char *number; /* Incoming phone number */ + int number_type; /* Incoming number type */ + guint ring_timer; /* For incoming call indication */ + const char *chld; /* Response to AT+CHLD=? */ +} ag = { + .telephony_ready = FALSE, + .features = 0, + .er_mode = 3, + .er_ind = 0, + .rh = BTRH_NOT_SUPPORTED, + .number = NULL, + .number_type = 0, + .ring_timer = 0, +}; + +static gboolean sco_hci = TRUE; +static gboolean fast_connectable = FALSE; + +static GSList *active_devices = NULL; + +static char *str_state[] = { + "HEADSET_STATE_DISCONNECTED", + "HEADSET_STATE_CONNECTING", + "HEADSET_STATE_CONNECTED", + "HEADSET_STATE_PLAY_IN_PROGRESS", + "HEADSET_STATE_PLAYING", +}; + +struct headset_state_callback { + headset_state_cb cb; + void *user_data; + unsigned int id; +}; + +struct headset_nrec_callback { + unsigned int id; + headset_nrec_cb cb; + void *user_data; +}; + +struct connect_cb { + unsigned int id; + headset_stream_cb_t cb; + void *cb_data; +}; + +struct pending_connect { + DBusMessage *msg; + DBusPendingCall *call; + GIOChannel *io; + int err; + headset_state_t target_state; + GSList *callbacks; + uint16_t svclass; +}; + +struct headset_slc { + char buf[BUF_SIZE]; + int data_start; + int data_length; + + gboolean cli_active; + gboolean cme_enabled; + gboolean cwa_enabled; + gboolean pending_ring; + gboolean inband_ring; + gboolean nrec; + gboolean nrec_req; + + int sp_gain; + int mic_gain; + + unsigned int hf_features; +}; + +struct headset { + uint32_t hsp_handle; + uint32_t hfp_handle; + + int rfcomm_ch; + + GIOChannel *rfcomm; + GIOChannel *tmp_rfcomm; + GIOChannel *sco; + guint sco_id; + + gboolean auto_dc; + + guint dc_timer; + + gboolean hfp_active; + gboolean search_hfp; + gboolean rfcomm_initiator; + + headset_state_t state; + struct pending_connect *pending; + + headset_lock_t lock; + struct headset_slc *slc; + GSList *nrec_cbs; +}; + +struct event { + const char *cmd; + int (*callback) (struct audio_device *device, const char *buf); +}; + +static GSList *headset_callbacks = NULL; + +static void error_connect_failed(DBusConnection *conn, DBusMessage *msg, + int err) +{ + DBusMessage *reply = btd_error_failed(msg, + err < 0 ? strerror(-err) : "Connect failed"); + g_dbus_send_message(conn, reply); +} + +static int rfcomm_connect(struct audio_device *device, headset_stream_cb_t cb, + void *user_data, unsigned int *cb_id); +static int get_records(struct audio_device *device, headset_stream_cb_t cb, + void *user_data, unsigned int *cb_id); + +static void print_ag_features(uint32_t features) +{ + GString *gstr; + char *str; + + if (features == 0) { + DBG("HFP AG features: (none)"); + return; + } + + gstr = g_string_new("HFP AG features: "); + + if (features & AG_FEATURE_THREE_WAY_CALLING) + g_string_append(gstr, "\"Three-way calling\" "); + if (features & AG_FEATURE_EC_ANDOR_NR) + g_string_append(gstr, "\"EC and/or NR function\" "); + if (features & AG_FEATURE_VOICE_RECOGNITION) + g_string_append(gstr, "\"Voice recognition function\" "); + if (features & AG_FEATURE_INBAND_RINGTONE) + g_string_append(gstr, "\"In-band ring tone capability\" "); + if (features & AG_FEATURE_ATTACH_NUMBER_TO_VOICETAG) + g_string_append(gstr, "\"Attach a number to a voice tag\" "); + if (features & AG_FEATURE_REJECT_A_CALL) + g_string_append(gstr, "\"Ability to reject a call\" "); + if (features & AG_FEATURE_ENHANCED_CALL_STATUS) + g_string_append(gstr, "\"Enhanced call status\" "); + if (features & AG_FEATURE_ENHANCED_CALL_CONTROL) + g_string_append(gstr, "\"Enhanced call control\" "); + if (features & AG_FEATURE_EXTENDED_ERROR_RESULT_CODES) + g_string_append(gstr, "\"Extended Error Result Codes\" "); + + str = g_string_free(gstr, FALSE); + + DBG("%s", str); + + g_free(str); +} + +static void print_hf_features(uint32_t features) +{ + GString *gstr; + char *str; + + if (features == 0) { + DBG("HFP HF features: (none)"); + return; + } + + gstr = g_string_new("HFP HF features: "); + + if (features & HF_FEATURE_EC_ANDOR_NR) + g_string_append(gstr, "\"EC and/or NR function\" "); + if (features & HF_FEATURE_CALL_WAITING_AND_3WAY) + g_string_append(gstr, "\"Call waiting and 3-way calling\" "); + if (features & HF_FEATURE_CLI_PRESENTATION) + g_string_append(gstr, "\"CLI presentation capability\" "); + if (features & HF_FEATURE_VOICE_RECOGNITION) + g_string_append(gstr, "\"Voice recognition activation\" "); + if (features & HF_FEATURE_REMOTE_VOLUME_CONTROL) + g_string_append(gstr, "\"Remote volume control\" "); + if (features & HF_FEATURE_ENHANCED_CALL_STATUS) + g_string_append(gstr, "\"Enhanced call status\" "); + if (features & HF_FEATURE_ENHANCED_CALL_CONTROL) + g_string_append(gstr, "\"Enhanced call control\" "); + + str = g_string_free(gstr, FALSE); + + DBG("%s", str); + + g_free(str); +} + +static const char *state2str(headset_state_t state) +{ + switch (state) { + case HEADSET_STATE_DISCONNECTED: + return "disconnected"; + case HEADSET_STATE_CONNECTING: + return "connecting"; + case HEADSET_STATE_CONNECTED: + case HEADSET_STATE_PLAY_IN_PROGRESS: + return "connected"; + case HEADSET_STATE_PLAYING: + return "playing"; + } + + return NULL; +} + +static int headset_send_valist(struct headset *hs, char *format, va_list ap) +{ + char rsp[BUF_SIZE]; + ssize_t total_written, count; + int fd; + + count = vsnprintf(rsp, sizeof(rsp), format, ap); + + if (count < 0) + return -EINVAL; + + if (!hs->rfcomm) { + error("headset_send: the headset is not connected"); + return -EIO; + } + + total_written = 0; + fd = g_io_channel_unix_get_fd(hs->rfcomm); + + while (total_written < count) { + ssize_t written; + + written = write(fd, rsp + total_written, + count - total_written); + if (written < 0) + return -errno; + + total_written += written; + } + + return 0; +} + +static int __attribute__((format(printf, 2, 3))) + headset_send(struct headset *hs, char *format, ...) +{ + va_list ap; + int ret; + + va_start(ap, format); + ret = headset_send_valist(hs, format, ap); + va_end(ap); + + return ret; +} + +static int supported_features(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + int err; + + if (strlen(buf) < 9) + return -EINVAL; + + slc->hf_features = strtoul(&buf[8], NULL, 10); + + print_hf_features(slc->hf_features); + + err = headset_send(hs, "\r\n+BRSF: %u\r\n", ag.features); + if (err < 0) + return err; + + return headset_send(hs, "\r\nOK\r\n"); +} + +static char *indicator_ranges(const struct indicator *indicators) +{ + int i; + GString *gstr; + + gstr = g_string_new("\r\n+CIND: "); + + for (i = 0; indicators[i].desc != NULL; i++) { + if (i == 0) + g_string_append_printf(gstr, "(\"%s\",(%s))", + indicators[i].desc, + indicators[i].range); + else + g_string_append_printf(gstr, ",(\"%s\",(%s))", + indicators[i].desc, + indicators[i].range); + } + + g_string_append(gstr, "\r\n"); + + return g_string_free(gstr, FALSE); +} + +static char *indicator_values(const struct indicator *indicators) +{ + int i; + GString *gstr; + + gstr = g_string_new("\r\n+CIND: "); + + for (i = 0; indicators[i].desc != NULL; i++) { + if (i == 0) + g_string_append_printf(gstr, "%d", indicators[i].val); + else + g_string_append_printf(gstr, ",%d", indicators[i].val); + } + + g_string_append(gstr, "\r\n"); + + return g_string_free(gstr, FALSE); +} + +static int report_indicators(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + int err; + char *str; + + if (strlen(buf) < 8) + return -EINVAL; + + if (ag.indicators == NULL) { + error("HFP AG indicators not initialized"); + return headset_send(hs, "\r\nERROR\r\n"); + } + + if (buf[7] == '=') + str = indicator_ranges(ag.indicators); + else + str = indicator_values(ag.indicators); + + err = headset_send(hs, "%s", str); + + g_free(str); + + if (err < 0) + return err; + + return headset_send(hs, "\r\nOK\r\n"); +} + +static void pending_connect_complete(struct connect_cb *cb, struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + if (hs->pending->err < 0) + cb->cb(NULL, cb->cb_data); + else + cb->cb(dev, cb->cb_data); +} + +static void pending_connect_finalize(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + struct pending_connect *p = hs->pending; + + if (p == NULL) + return; + + if (p->svclass) + bt_cancel_discovery(&dev->src, &dev->dst); + + g_slist_foreach(p->callbacks, (GFunc) pending_connect_complete, dev); + + g_slist_free_full(p->callbacks, g_free); + + if (p->io) { + g_io_channel_shutdown(p->io, TRUE, NULL); + g_io_channel_unref(p->io); + } + + if (p->msg) + dbus_message_unref(p->msg); + + if (p->call) { + dbus_pending_call_cancel(p->call); + dbus_pending_call_unref(p->call); + } + + g_free(p); + + hs->pending = NULL; +} + +static void pending_connect_init(struct headset *hs, headset_state_t target_state) +{ + if (hs->pending) { + if (hs->pending->target_state < target_state) + hs->pending->target_state = target_state; + return; + } + + hs->pending = g_new0(struct pending_connect, 1); + hs->pending->target_state = target_state; +} + +static unsigned int connect_cb_new(struct headset *hs, + headset_state_t target_state, + headset_stream_cb_t func, + void *user_data) +{ + struct connect_cb *cb; + static unsigned int free_cb_id = 1; + + pending_connect_init(hs, target_state); + + if (!func) + return 0; + + cb = g_new(struct connect_cb, 1); + + cb->cb = func; + cb->cb_data = user_data; + cb->id = free_cb_id++; + + hs->pending->callbacks = g_slist_append(hs->pending->callbacks, + cb); + + return cb->id; +} + +static void __attribute__((format(printf, 3, 4))) + send_foreach_headset(GSList *devices, + int (*cmp) (struct headset *hs), + char *format, ...) +{ + GSList *l; + va_list ap; + + for (l = devices; l != NULL; l = l->next) { + struct audio_device *device = l->data; + struct headset *hs = device->headset; + int ret; + + assert(hs != NULL); + + if (cmp && cmp(hs) != 0) + continue; + + va_start(ap, format); + ret = headset_send_valist(hs, format, ap); + if (ret < 0) + error("Failed to send to headset: %s (%d)", + strerror(-ret), -ret); + va_end(ap); + } +} + +static int cli_cmp(struct headset *hs) +{ + struct headset_slc *slc = hs->slc; + + if (!hs->hfp_active) + return -1; + + if (slc->cli_active) + return 0; + else + return -1; +} + +static gboolean ring_timer_cb(gpointer data) +{ + send_foreach_headset(active_devices, NULL, "\r\nRING\r\n"); + + if (ag.number) + send_foreach_headset(active_devices, cli_cmp, + "\r\n+CLIP: \"%s\",%d\r\n", + ag.number, ag.number_type); + + return TRUE; +} + +static void sco_connect_cb(GIOChannel *chan, GError *err, gpointer user_data) +{ + int sk; + struct audio_device *dev = user_data; + struct headset *hs = dev->headset; + struct headset_slc *slc = hs->slc; + struct pending_connect *p = hs->pending; + + if (err) { + error("%s", err->message); + + if (p != NULL) { + p->err = -errno; + if (p->msg) + error_connect_failed(dev->conn, p->msg, p->err); + pending_connect_finalize(dev); + } + + if (hs->rfcomm) + headset_set_state(dev, HEADSET_STATE_CONNECTED); + else + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); + + return; + } + + DBG("SCO socket opened for headset %s", dev->path); + + sk = g_io_channel_unix_get_fd(chan); + + DBG("SCO fd=%d", sk); + + if (p) { + p->io = NULL; + if (p->msg) { + DBusMessage *reply; + reply = dbus_message_new_method_return(p->msg); + g_dbus_send_message(dev->conn, reply); + } + + pending_connect_finalize(dev); + } + + fcntl(sk, F_SETFL, 0); + + headset_set_state(dev, HEADSET_STATE_PLAYING); + + if (slc->pending_ring) { + ring_timer_cb(NULL); + ag.ring_timer = g_timeout_add_seconds(RING_INTERVAL, + ring_timer_cb, + NULL); + slc->pending_ring = FALSE; + } +} + +static int sco_connect(struct audio_device *dev, headset_stream_cb_t cb, + void *user_data, unsigned int *cb_id) +{ + struct headset *hs = dev->headset; + GError *err = NULL; + GIOChannel *io; + + if (hs->state != HEADSET_STATE_CONNECTED) + return -EINVAL; + + io = bt_io_connect(BT_IO_SCO, sco_connect_cb, dev, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &dev->src, + BT_IO_OPT_DEST_BDADDR, &dev->dst, + BT_IO_OPT_INVALID); + if (!io) { + error("%s", err->message); + g_error_free(err); + return -EIO; + } + + hs->sco = io; + + headset_set_state(dev, HEADSET_STATE_PLAY_IN_PROGRESS); + + pending_connect_init(hs, HEADSET_STATE_PLAYING); + + if (cb) { + unsigned int id = connect_cb_new(hs, HEADSET_STATE_PLAYING, + cb, user_data); + if (cb_id) + *cb_id = id; + } + + return 0; +} + +static int hfp_cmp(struct headset *hs) +{ + if (hs->hfp_active) + return 0; + else + return -1; +} + +static void hfp_slc_complete(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + struct pending_connect *p = hs->pending; + + DBG("HFP Service Level Connection established"); + + headset_set_state(dev, HEADSET_STATE_CONNECTED); + + if (p == NULL) + return; + + if (p->target_state == HEADSET_STATE_CONNECTED) { + if (p->msg) { + DBusMessage *reply = dbus_message_new_method_return(p->msg); + g_dbus_send_message(dev->conn, reply); + } + pending_connect_finalize(dev); + return; + } + + p->err = sco_connect(dev, NULL, NULL, NULL); + if (p->err < 0) { + if (p->msg) + error_connect_failed(dev->conn, p->msg, p->err); + pending_connect_finalize(dev); + } +} + +static int telephony_generic_rsp(struct audio_device *device, cme_error_t err) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + +#ifdef __TIZEN_PATCH__ + if (!slc) + return -EIO; +#endif + if ((err != CME_ERROR_NONE) && slc->cme_enabled) + return headset_send(hs, "\r\n+CME ERROR: %d\r\n", err); + + switch (err) { + case CME_ERROR_NONE: + return headset_send(hs, "\r\nOK\r\n"); + case CME_ERROR_NO_NETWORK_SERVICE: + return headset_send(hs, "\r\nNO CARRIER\r\n"); + default: + return headset_send(hs, "\r\nERROR\r\n"); + } +} + +int telephony_event_reporting_rsp(void *telephony_device, cme_error_t err) +{ + struct audio_device *device = telephony_device; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + int ret; + + if (err != CME_ERROR_NONE) + return telephony_generic_rsp(telephony_device, err); + + ret = headset_send(hs, "\r\nOK\r\n"); + if (ret < 0) + return ret; + + if (hs->state != HEADSET_STATE_CONNECTING) + return 0; + + if (slc->hf_features & HF_FEATURE_CALL_WAITING_AND_3WAY && + ag.features & AG_FEATURE_THREE_WAY_CALLING) + return 0; + + hfp_slc_complete(device); + + return 0; +} + +static int event_reporting(struct audio_device *dev, const char *buf) +{ + char **tokens; /* , , , , */ + + if (strlen(buf) < 13) + return -EINVAL; + + tokens = g_strsplit(&buf[8], ",", 5); + if (g_strv_length(tokens) < 4) { + g_strfreev(tokens); + return -EINVAL; + } + + ag.er_mode = atoi(tokens[0]); + ag.er_ind = atoi(tokens[3]); + + g_strfreev(tokens); + tokens = NULL; + + DBG("Event reporting (CMER): mode=%d, ind=%d", + ag.er_mode, ag.er_ind); + + switch (ag.er_ind) { + case 0: + case 1: + telephony_event_reporting_req(dev, ag.er_ind); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int call_hold(struct audio_device *dev, const char *buf) +{ + struct headset *hs = dev->headset; + int err; + + if (strlen(buf) < 9) + return -EINVAL; + + if (buf[8] != '?') { + telephony_call_hold_req(dev, &buf[8]); + return 0; + } + + err = headset_send(hs, "\r\n+CHLD: (%s)\r\n", ag.chld); + if (err < 0) + return err; + + err = headset_send(hs, "\r\nOK\r\n"); + if (err < 0) + return err; + + if (hs->state != HEADSET_STATE_CONNECTING) + return 0; + + hfp_slc_complete(dev); + + return 0; +} + +int telephony_key_press_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int key_press(struct audio_device *device, const char *buf) +{ + if (strlen(buf) < 9) + return -EINVAL; + + g_dbus_emit_signal(device->conn, device->path, + AUDIO_HEADSET_INTERFACE, "AnswerRequested", + DBUS_TYPE_INVALID); + + if (ag.ring_timer) { + g_source_remove(ag.ring_timer); + ag.ring_timer = 0; + } + + telephony_key_press_req(device, &buf[8]); + + return 0; +} + +int telephony_answer_call_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int answer_call(struct audio_device *device, const char *buf) +{ + if (ag.ring_timer) { + g_source_remove(ag.ring_timer); + ag.ring_timer = 0; + } + + if (ag.number) { + g_free(ag.number); + ag.number = NULL; + } + + telephony_answer_call_req(device); + + return 0; +} + +int telephony_terminate_call_rsp(void *telephony_device, + cme_error_t err) +{ + struct audio_device *device = telephony_device; + struct headset *hs = device->headset; + + if (err != CME_ERROR_NONE) + return telephony_generic_rsp(telephony_device, err); + + g_dbus_emit_signal(device->conn, device->path, + AUDIO_HEADSET_INTERFACE, "CallTerminated", + DBUS_TYPE_INVALID); + + return headset_send(hs, "\r\nOK\r\n"); +} + +static int terminate_call(struct audio_device *device, const char *buf) +{ + if (ag.number) { + g_free(ag.number); + ag.number = NULL; + } + + if (ag.ring_timer) { + g_source_remove(ag.ring_timer); + ag.ring_timer = 0; + } + + telephony_terminate_call_req(device); + + return 0; +} + +static int cli_notification(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (strlen(buf) < 9) + return -EINVAL; + + slc->cli_active = buf[8] == '1' ? TRUE : FALSE; + + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_response_and_hold_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int response_and_hold(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + + if (strlen(buf) < 8) + return -EINVAL; + + if (ag.rh == BTRH_NOT_SUPPORTED) + return telephony_generic_rsp(device, CME_ERROR_NOT_SUPPORTED); + + if (buf[7] == '=') { + telephony_response_and_hold_req(device, atoi(&buf[8]) < 0); + return 0; + } + + if (ag.rh >= 0) + headset_send(hs, "\r\n+BTRH: %d\r\n", ag.rh); + + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_last_dialed_number_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int last_dialed_number(struct audio_device *device, const char *buf) +{ + telephony_last_dialed_number_req(device); + + return 0; +} + +int telephony_dial_number_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int dial_number(struct audio_device *device, const char *buf) +{ + char number[BUF_SIZE]; + size_t buf_len; + + buf_len = strlen(buf); + + if (buf[buf_len - 1] != ';') { + DBG("Rejecting non-voice call dial request"); + return -EINVAL; + } + + memset(number, 0, sizeof(number)); + strncpy(number, &buf[3], buf_len - 4); + + telephony_dial_number_req(device, number); + + return 0; +} + +static int headset_set_gain(struct audio_device *device, uint16_t gain, char type) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + const char *name, *property; + + if (gain > 15) { + error("Invalid gain value: %u", gain); + return -EINVAL; + } + + switch (type) { + case HEADSET_GAIN_SPEAKER: + if (slc->sp_gain == gain) { + DBG("Ignoring no-change in speaker gain"); + return -EALREADY; + } + name = "SpeakerGainChanged"; + property = "SpeakerGain"; + slc->sp_gain = gain; + break; + case HEADSET_GAIN_MICROPHONE: + if (slc->mic_gain == gain) { + DBG("Ignoring no-change in microphone gain"); + return -EALREADY; + } + name = "MicrophoneGainChanged"; + property = "MicrophoneGain"; + slc->mic_gain = gain; + break; + default: + error("Unknown gain setting"); + return -EINVAL; + } + + g_dbus_emit_signal(device->conn, device->path, + AUDIO_HEADSET_INTERFACE, name, + DBUS_TYPE_UINT16, &gain, + DBUS_TYPE_INVALID); + + emit_property_changed(device->conn, device->path, + AUDIO_HEADSET_INTERFACE, property, + DBUS_TYPE_UINT16, &gain); + + return 0; +} + +static int signal_gain_setting(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + dbus_uint16_t gain; + int err; + + if (strlen(buf) < 8) { + error("Too short string for Gain setting"); + return -EINVAL; + } + + gain = (dbus_uint16_t) strtol(&buf[7], NULL, 10); + + err = headset_set_gain(device, gain, buf[5]); + if (err < 0 && err != -EALREADY) + return err; + + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_transmit_dtmf_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int dtmf_tone(struct audio_device *device, const char *buf) +{ + char tone; + + if (strlen(buf) < 8) { + error("Too short string for DTMF tone"); + return -EINVAL; + } + + tone = buf[7]; + if (tone >= '#' && tone <= 'D') + telephony_transmit_dtmf_req(device, tone); + else + return -EINVAL; + + return 0; +} + +int telephony_subscriber_number_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int subscriber_number(struct audio_device *device, const char *buf) +{ + telephony_subscriber_number_req(device); + + return 0; +} + +int telephony_list_current_calls_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int list_current_calls(struct audio_device *device, const char *buf) +{ + telephony_list_current_calls_req(device); + + return 0; +} + +static int extended_errors(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (strlen(buf) < 9) + return -EINVAL; + + if (buf[8] == '1') { + slc->cme_enabled = TRUE; + DBG("CME errors enabled for headset %p", hs); + } else { + slc->cme_enabled = FALSE; + DBG("CME errors disabled for headset %p", hs); + } + + return headset_send(hs, "\r\nOK\r\n"); +} + +static int call_waiting_notify(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (strlen(buf) < 9) + return -EINVAL; + + if (buf[8] == '1') { + slc->cwa_enabled = TRUE; + DBG("Call waiting notification enabled for headset %p", hs); + } else { + slc->cwa_enabled = FALSE; + DBG("Call waiting notification disabled for headset %p", hs); + } + + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_operator_selection_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +int telephony_call_hold_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +int telephony_nr_and_ec_rsp(void *telephony_device, cme_error_t err) +{ + struct audio_device *device = telephony_device; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (err == CME_ERROR_NONE) { + GSList *l; + + for (l = hs->nrec_cbs; l; l = l->next) { + struct headset_nrec_callback *nrec_cb = l->data; + + nrec_cb->cb(device, slc->nrec_req, nrec_cb->user_data); + } + + slc->nrec = hs->slc->nrec_req; + } + + return telephony_generic_rsp(telephony_device, err); +} + +int telephony_voice_dial_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +int telephony_operator_selection_ind(int mode, const char *oper) +{ + if (!active_devices) + return -ENODEV; + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+COPS: %d,0,\"%s\"\r\n", + mode, oper); + return 0; +} + +static int operator_selection(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + + if (strlen(buf) < 8) + return -EINVAL; + + switch (buf[7]) { + case '?': + telephony_operator_selection_req(device); + break; + case '=': + return headset_send(hs, "\r\nOK\r\n"); + default: + return -EINVAL; + } + + return 0; +} + +static int nr_and_ec(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (strlen(buf) < 9) + return -EINVAL; + + if (buf[8] == '0') + slc->nrec_req = FALSE; + else + slc->nrec_req = TRUE; + + telephony_nr_and_ec_req(device, slc->nrec_req); + + return 0; +} + +static int voice_dial(struct audio_device *device, const char *buf) +{ + gboolean enable; + + if (strlen(buf) < 9) + return -EINVAL; + + if (buf[8] == '0') + enable = FALSE; + else + enable = TRUE; + + telephony_voice_dial_req(device, enable); + + return 0; +} + +#ifdef __TIZEN_PATCH__ +int telephony_select_phonebook_memory_status_rsp(void *telephony_device, const char *path, + uint32_t total, uint32_t used, + cme_error_t err) +{ + struct audio_device *device = telephony_device; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (err != CME_ERROR_NONE) { + if (slc->cme_enabled) + return headset_send(hs, "\r\n+CME ERROR: %d\r\n", err); + else + return headset_send(hs, "\r\nERROR\r\n"); + } + + if (!active_devices) + return -ENODEV; + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CPBS: %s,%d,%d\r\n", + path, used, total); + + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_select_phonebook_memory_list_rsp(void *telephony_device, const char *buf, + cme_error_t err) +{ + struct audio_device *device = telephony_device; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if ((err != CME_ERROR_NONE) || (NULL == buf)) { + if (slc->cme_enabled) + return headset_send(hs, "\r\n+CME ERROR: %d\r\n", err); + else + return headset_send(hs, "\r\nERROR\r\n"); + } + + if (NULL != buf) { + if (!active_devices) + return -ENODEV; + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CPBS: %s\r\n", buf); + + } + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_select_phonebook_memory_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int select_phonebook_memory(struct audio_device *device, const char *buf) +{ + if (strlen(buf) < 8) + return -EINVAL; + + if (buf[7] == '?') { + telephony_select_phonebook_memory_status(device); + return 0; + } + + if (buf[7] == '=') { + if (buf[8] == '?') { + telephony_select_phonebook_memory_list(device); + return 0; + } + telephony_select_phonebook_memory(device, &buf[8]); + return 0; + } + + return -EINVAL; +} + +int telephony_read_phonebook_entries_list_rsp(void *telephony_device, + uint32_t used, + uint32_t number_length, + uint32_t name_length, + cme_error_t err) +{ + struct audio_device *device = telephony_device; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + int send_err = 0; + int index = 1; + + if (err != CME_ERROR_NONE) { + if (slc->cme_enabled) + return headset_send(hs, "\r\n+CME ERROR: %d\r\n", err); + else + return headset_send(hs, "\r\nERROR\r\n"); + } + + if (used < 1) + index = 0; + + send_err = headset_send(hs, "\r\n+CPBR: (%d-%d),%d,%d\r\n", + index, used, number_length, name_length); + if (send_err < 0) + return send_err; + + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_read_phonebook_entries_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +int telephony_read_phonebook_entries_ind(const char *name, const char *number, + uint32_t handle) +{ + int type = 129; + const char *pos = NULL; + + pos = number; + while (*pos == ' ' || *pos == '\t') + pos++; + + /* 145 means international access code, otherwise 129 is used */ + if (*pos == '+') + type = 145; + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CPBR: %d,\"%s\",%d,\"%s\"\r\n", + handle, number, type, name); + return 0; +} + +static int read_phonebook_entries(struct audio_device *device, const char *buf) +{ + if (strlen(buf) < 8) + return -EINVAL; + + if (buf[7] != '=') + return -EINVAL; + + if (buf[8] == '?') + telephony_read_phonebook_entries_list(device); + else + telephony_read_phonebook_entries(device, &buf[8]); + + return 0; +} + +int telephony_find_phonebook_entries_status_rsp(void *telephony_device, + cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +int telephony_find_phonebook_entries_rsp(void *telephony_device, + cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +int telephony_find_phonebook_entries_status_ind( uint32_t number_length, + uint32_t name_length) +{ + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CPBF: %d,%d\r\n", + number_length, name_length); + + return 0; +} + +int telephony_find_phonebook_entries_ind(const char *name, const char *number, + uint32_t handle) +{ + int type = 129; + const char *pos = NULL; + + pos = number; + while (*pos == ' ' || *pos == '\t') + pos++; + + /* 145 means international access code, otherwise 129 is used */ + if (*pos == '+') + type = 145; + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CPBF: %d,\"%s\",%d,\"%s\"\r\n", + handle, number, type, name); + return 0; +} + +static int find_phonebook_entires(struct audio_device *device, const char *buf) +{ + if (strlen(buf) < 8) + return -EINVAL; + + if (buf[7] != '=') + return -EINVAL; + + if (buf[8] == '?') + telephony_find_phonebook_entries_status(device); + else + telephony_find_phonebook_entries(device, &buf[8]); + + return 0; +} + +int telephony_list_preffered_store_rsp(void *telephony_device, + char *prefrd_list, + cme_error_t err) +{ + struct audio_device *device = telephony_device; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (err != CME_ERROR_NONE) { + if (slc->cme_enabled) + return headset_send(hs, "\r\n+CME ERROR: %d\r\n", err); + else + return headset_send(hs, "\r\nERROR\r\n"); + } + + if (NULL != prefrd_list) { + if (!active_devices) + return -ENODEV; + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CPMS: %s\r\n", prefrd_list); + } + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_get_preffered_store_capacity_rsp(void *telephony_device, + uint32_t store_capacity, + cme_error_t err) +{ + struct audio_device *device = telephony_device; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (err != CME_ERROR_NONE) { + if (slc->cme_enabled) + return headset_send(hs, "\r\n+CME ERROR: %d\r\n", err); + else + return headset_send(hs, "\r\nERROR\r\n"); + } + + if (0 != store_capacity) { + if (!active_devices) + return -ENODEV; + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CPMS: %d\r\n", store_capacity); + } + return headset_send(hs, "\r\nOK\r\n"); +} + +static int preffered_message_storage(struct audio_device *device, const char *buf) +{ + struct headset *hs = device->headset; + + if (NULL != buf) { + if (strlen(buf) < 9) + return -EINVAL; + + if (buf[7] == '?') { + telephony_get_preffered_store_capacity(device); + return 0; + } + + if (buf[8] == '=') { + if (buf[9] == '?') { + telephony_list_preffered_store(device); + } else { + /* telephony_set_preffered_store_capcity(device, &buf[9]); */ + return headset_send(hs, "\r\nOK\r\n"); + } + } + } + return 0; +} + +int telephony_supported_character_generic_rsp(void *telephony_device, + char *character_set_list, + cme_error_t err) +{ + struct audio_device *device = telephony_device; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + + if (err != CME_ERROR_NONE) { + if (slc->cme_enabled) + return headset_send(hs, "\r\n+CME ERROR: %d\r\n", err); + else + return headset_send(hs, "\r\nERROR\r\n"); + } + + if (NULL != character_set_list) { + if (!active_devices) + return -ENODEV; + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CSCS: %s\r\n", character_set_list); + } + return headset_send(hs, "\r\nOK\r\n"); +} + +int telephony_set_characterset_generic_rsp(void *telephony_device, cme_error_t err) +{ + return telephony_generic_rsp(telephony_device, err); +} + +static int select_character_set(struct audio_device *device, const char *buf) +{ + if (NULL != buf) { + if (strlen(buf) < 7) + return -EINVAL; + + if (buf[7] == '?') { + telephony_get_character_set(device); + return 0; + } + + if (buf[7] == '=') { + if (buf[8] == '?') { + telephony_list_supported_character(device); + } else { + telephony_set_characterset(device, &buf[8]); + } + } + } + return 0; + +} + +int telephony_battery_charge_status_rsp(void *telephony_device, + int32_t bcs, + int32_t bcl, + cme_error_t err) +{ + struct audio_device *device = telephony_device; + + if (err == CME_ERROR_NONE) { + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CBC: %d,%d\r\n", bcs, bcl); + } + + return telephony_generic_rsp(device, err); +} + +static int get_battery_charge_status(struct audio_device *device, const char *buf) +{ + if (strlen(buf) < 6) + return -EINVAL; + + if (buf[6] == '=') + return telephony_generic_rsp(device,CME_ERROR_NONE); + + telephony_get_battery_property(device); + return 0; +} + +int telephony_signal_quality_list_supported_rsp(void *telephony_device, + cme_error_t err) +{ + struct audio_device *device = telephony_device; + + if (err == CME_ERROR_NONE) { + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CSQ: (0-31,99),(99)\r\n"); + } + return telephony_generic_rsp(device,err); +} + +int telephony_signal_quality_rsp(void *telephony_device, + int32_t rssi, + int32_t ber, + cme_error_t err) +{ + struct audio_device *device = telephony_device; + + if (err == CME_ERROR_NONE) { + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CSQ: %d,%d\r\n", rssi, ber); + } + return telephony_generic_rsp(device,err); +} + +static int get_signal_quality(struct audio_device *device, const char *buf) +{ + if (strlen(buf) < 6) + return -EINVAL; + + if (buf[6] == '=') + telephony_signal_quality_list_supported_rsp(device, + CME_ERROR_NONE); + else + telephony_get_signal_quality(device); + + return 0; + +} + +#endif + +static int apple_command(struct audio_device *device, const char *buf) +{ + DBG("Got Apple command: %s", buf); + + return telephony_generic_rsp(device, CME_ERROR_NONE); +} + +static struct event event_callbacks[] = { + { "ATA", answer_call }, + { "ATD", dial_number }, + { "AT+VG", signal_gain_setting }, + { "AT+BRSF", supported_features }, + { "AT+CIND", report_indicators }, + { "AT+CMER", event_reporting }, + { "AT+CHLD", call_hold }, + { "AT+CHUP", terminate_call }, + { "AT+CKPD", key_press }, + { "AT+CLIP", cli_notification }, + { "AT+BTRH", response_and_hold }, + { "AT+BLDN", last_dialed_number }, + { "AT+VTS", dtmf_tone }, + { "AT+CNUM", subscriber_number }, + { "AT+CLCC", list_current_calls }, + { "AT+CMEE", extended_errors }, + { "AT+CCWA", call_waiting_notify }, + { "AT+COPS", operator_selection }, + { "AT+NREC", nr_and_ec }, + { "AT+BVRA", voice_dial }, + { "AT+XAPL", apple_command }, + { "AT+IPHONEACCEV", apple_command }, +#ifdef __TIZEN_PATCH__ + { "AT+CPBS", select_phonebook_memory }, + { "AT+CPBR", read_phonebook_entries}, + { "AT+CPBF", find_phonebook_entires }, + { "AT+CSCS", select_character_set }, + { "AT+CSQ", get_signal_quality }, + { "AT+CBC", get_battery_charge_status }, +#endif + { 0 } +}; + +static int handle_event(struct audio_device *device, const char *buf) +{ + struct event *ev; + + DBG("Received %s", buf); + + for (ev = event_callbacks; ev->cmd; ev++) { + if (!strncmp(buf, ev->cmd, strlen(ev->cmd))) + return ev->callback(device, buf); + } + + return -EINVAL; +} + +static void close_sco(struct audio_device *device) +{ + struct headset *hs = device->headset; + + if (hs->sco) { + int sock = g_io_channel_unix_get_fd(hs->sco); + shutdown(sock, SHUT_RDWR); + g_io_channel_shutdown(hs->sco, TRUE, NULL); + g_io_channel_unref(hs->sco); + hs->sco = NULL; + } + + if (hs->sco_id) { + g_source_remove(hs->sco_id); + hs->sco_id = 0; + } +} + +static gboolean rfcomm_io_cb(GIOChannel *chan, GIOCondition cond, + struct audio_device *device) +{ + struct headset *hs; + struct headset_slc *slc; + unsigned char buf[BUF_SIZE]; + ssize_t bytes_read; + size_t free_space; + int fd; + + if (cond & G_IO_NVAL) + return FALSE; + + hs = device->headset; + slc = hs->slc; + + if (cond & (G_IO_ERR | G_IO_HUP)) { + DBG("ERR or HUP on RFCOMM socket"); + goto failed; + } + + fd = g_io_channel_unix_get_fd(chan); + + bytes_read = read(fd, buf, sizeof(buf) - 1); + if (bytes_read < 0) + return TRUE; + + free_space = sizeof(slc->buf) - slc->data_start - + slc->data_length - 1; + + if (free_space < (size_t) bytes_read) { + /* Very likely that the HS is sending us garbage so + * just ignore the data and disconnect */ + error("Too much data to fit incoming buffer"); + goto failed; + } + + memcpy(&slc->buf[slc->data_start], buf, bytes_read); + slc->data_length += bytes_read; + + /* Make sure the data is null terminated so we can use string + * functions */ + slc->buf[slc->data_start + slc->data_length] = '\0'; + + while (slc->data_length > 0) { + char *cr; + int err; + off_t cmd_len; + + cr = strchr(&slc->buf[slc->data_start], '\r'); + if (!cr) + break; + + cmd_len = 1 + (off_t) cr - (off_t) &slc->buf[slc->data_start]; + *cr = '\0'; + + if (cmd_len > 1) + err = handle_event(device, &slc->buf[slc->data_start]); + else + /* Silently skip empty commands */ + err = 0; + + if (err == -EINVAL) { + error("Badly formated or unrecognized command: %s", + &slc->buf[slc->data_start]); + err = telephony_generic_rsp(device, + CME_ERROR_NOT_SUPPORTED); + if (err < 0) + goto failed; + } else if (err < 0) + error("Error handling command %s: %s (%d)", + &slc->buf[slc->data_start], + strerror(-err), -err); + + slc->data_start += cmd_len; + slc->data_length -= cmd_len; + + if (!slc->data_length) + slc->data_start = 0; + } + + return TRUE; + +failed: + headset_set_state(device, HEADSET_STATE_DISCONNECTED); + + return FALSE; +} + +static gboolean sco_cb(GIOChannel *chan, GIOCondition cond, + struct audio_device *device) +{ + if (cond & G_IO_NVAL) + return FALSE; + + error("Audio connection got disconnected"); + + pending_connect_finalize(device); + headset_set_state(device, HEADSET_STATE_CONNECTED); + + return FALSE; +} + +void headset_connect_cb(GIOChannel *chan, GError *err, gpointer user_data) +{ + struct audio_device *dev = user_data; + struct headset *hs = dev->headset; + struct pending_connect *p = hs->pending; + char hs_address[18]; + + if (err) { + error("%s", err->message); + goto failed; + } + + /* For HFP telephony isn't ready just disconnect */ + if (hs->hfp_active && !ag.telephony_ready) { + error("Unable to accept HFP connection since the telephony " + "subsystem isn't initialized"); + goto failed; + } + + hs->rfcomm = hs->tmp_rfcomm; + hs->tmp_rfcomm = NULL; + + ba2str(&dev->dst, hs_address); + + if (p) + p->io = NULL; + else + hs->auto_dc = FALSE; + + g_io_add_watch(chan, G_IO_IN | G_IO_ERR | G_IO_HUP| G_IO_NVAL, + (GIOFunc) rfcomm_io_cb, dev); + + DBG("%s: Connected to %s", dev->path, hs_address); + + hs->slc = g_new0(struct headset_slc, 1); + hs->slc->sp_gain = 15; + hs->slc->mic_gain = 15; + hs->slc->nrec = TRUE; + + /* In HFP mode wait for Service Level Connection */ + if (hs->hfp_active) + return; + + headset_set_state(dev, HEADSET_STATE_CONNECTED); + + if (p && p->target_state == HEADSET_STATE_PLAYING) { + p->err = sco_connect(dev, NULL, NULL, NULL); + if (p->err < 0) + goto failed; + return; + } + + if (p && p->msg) { + DBusMessage *reply = dbus_message_new_method_return(p->msg); + g_dbus_send_message(dev->conn, reply); + } + + pending_connect_finalize(dev); + + return; + +failed: + if (p && p->msg) + error_connect_failed(dev->conn, p->msg, p->err); + pending_connect_finalize(dev); + if (hs->rfcomm) + headset_set_state(dev, HEADSET_STATE_CONNECTED); + else + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); +} + +static int headset_set_channel(struct headset *headset, + const sdp_record_t *record, uint16_t svc) +{ + int ch; + sdp_list_t *protos; + + if (sdp_get_access_protos(record, &protos) < 0) { + error("Unable to get access protos from headset record"); + return -1; + } + + ch = sdp_get_proto_port(protos, RFCOMM_UUID); + + sdp_list_foreach(protos, (sdp_list_func_t) sdp_list_free, NULL); + sdp_list_free(protos, NULL); + + if (ch <= 0) { + error("Unable to get RFCOMM channel from Headset record"); + return -1; + } + + headset->rfcomm_ch = ch; + + if (svc == HANDSFREE_SVCLASS_ID) { + headset->hfp_handle = record->handle; + headset->hsp_handle = 0; + DBG("Discovered Handsfree service on channel %d", ch); + } else { + headset->hsp_handle = record->handle; + headset->hfp_handle = 0; + DBG("Discovered Headset service on channel %d", ch); + } + + return 0; +} + +static void get_record_cb(sdp_list_t *recs, int err, gpointer user_data) +{ + struct audio_device *dev = user_data; + struct headset *hs = dev->headset; + struct pending_connect *p = hs->pending; + sdp_record_t *record = NULL; + sdp_list_t *r; + uuid_t uuid; + + assert(hs->pending != NULL); + + if (err < 0) { + error("Unable to get service record: %s (%d)", + strerror(-err), -err); + p->err = -err; + if (p->msg) + error_connect_failed(dev->conn, p->msg, p->err); + goto failed; + } + + if (!recs || !recs->data) { + error("No records found"); + goto failed_not_supported; + } + + sdp_uuid16_create(&uuid, p->svclass); + + for (r = recs; r != NULL; r = r->next) { + sdp_list_t *classes; + uuid_t class; + + record = r->data; + + if (sdp_get_service_classes(record, &classes) < 0) { + error("Unable to get service classes from record"); + continue; + } + + memcpy(&class, classes->data, sizeof(uuid)); + + sdp_list_free(classes, free); + + if (sdp_uuid_cmp(&class, &uuid) == 0) + break; + } + + if (r == NULL) { + error("No record found with UUID 0x%04x", p->svclass); + goto failed_not_supported; + } + + if (headset_set_channel(hs, record, p->svclass) < 0) { + error("Unable to extract RFCOMM channel from service record"); + goto failed_not_supported; + } + + /* Set svclass to 0 so we can easily check that SDP is no-longer + * going on (to know if bt_cancel_discovery needs to be called) */ + p->svclass = 0; + + err = rfcomm_connect(dev, NULL, NULL, NULL); + if (err < 0) { + error("Unable to connect: %s (%d)", strerror(-err), -err); + p->err = -err; + if (p->msg != NULL) + error_connect_failed(dev->conn, p->msg, p->err); + goto failed; + } + + return; + +failed_not_supported: + if (p->svclass == HANDSFREE_SVCLASS_ID && + get_records(dev, NULL, NULL, NULL) == 0) + return; + if (p->msg) { + DBusMessage *reply = btd_error_not_supported(p->msg); + g_dbus_send_message(dev->conn, reply); + } +failed: + p->svclass = 0; + pending_connect_finalize(dev); + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); +} + +static int get_records(struct audio_device *device, headset_stream_cb_t cb, + void *user_data, unsigned int *cb_id) +{ + struct headset *hs = device->headset; + uint16_t svclass; + uuid_t uuid; + int err; + + if (hs->pending && hs->pending->svclass == HANDSFREE_SVCLASS_ID) + svclass = HEADSET_SVCLASS_ID; + else + svclass = hs->search_hfp ? HANDSFREE_SVCLASS_ID : + HEADSET_SVCLASS_ID; + + sdp_uuid16_create(&uuid, svclass); + + err = bt_search_service(&device->src, &device->dst, &uuid, + get_record_cb, device, NULL); + if (err < 0) + return err; + + if (hs->pending) { + hs->pending->svclass = svclass; + return 0; + } + + headset_set_state(device, HEADSET_STATE_CONNECTING); + + pending_connect_init(hs, HEADSET_STATE_CONNECTED); + + hs->pending->svclass = svclass; + + if (cb) { + unsigned int id; + id = connect_cb_new(hs, HEADSET_STATE_CONNECTED, + cb, user_data); + if (cb_id) + *cb_id = id; + } + + return 0; +} + +static int rfcomm_connect(struct audio_device *dev, headset_stream_cb_t cb, + void *user_data, unsigned int *cb_id) +{ + struct headset *hs = dev->headset; + char address[18]; + GError *err = NULL; + + if (!manager_allow_headset_connection(dev)) + return -ECONNREFUSED; + + if (hs->rfcomm_ch < 0) + return get_records(dev, cb, user_data, cb_id); + + ba2str(&dev->dst, address); + + DBG("%s: Connecting to %s channel %d", dev->path, address, + hs->rfcomm_ch); + + hs->tmp_rfcomm = bt_io_connect(BT_IO_RFCOMM, headset_connect_cb, dev, + NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &dev->src, + BT_IO_OPT_DEST_BDADDR, &dev->dst, + BT_IO_OPT_CHANNEL, hs->rfcomm_ch, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_INVALID); + + hs->rfcomm_ch = -1; + + if (!hs->tmp_rfcomm) { + error("%s", err->message); + g_error_free(err); + return -EIO; + } + + hs->hfp_active = hs->hfp_handle != 0 ? TRUE : FALSE; + hs->rfcomm_initiator = FALSE; + + headset_set_state(dev, HEADSET_STATE_CONNECTING); + + pending_connect_init(hs, HEADSET_STATE_CONNECTED); + + if (cb) { + unsigned int id = connect_cb_new(hs, HEADSET_STATE_CONNECTED, + cb, user_data); + if (cb_id) + *cb_id = id; + } + + return 0; +} + +static DBusMessage *hs_stop(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + DBusMessage *reply = NULL; + + if (hs->state < HEADSET_STATE_PLAY_IN_PROGRESS) + return btd_error_not_connected(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + headset_set_state(device, HEADSET_STATE_CONNECTED); + + return reply; +} + +static DBusMessage *hs_is_playing(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + DBusMessage *reply; + dbus_bool_t playing; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + playing = (hs->state == HEADSET_STATE_PLAYING); + + dbus_message_append_args(reply, DBUS_TYPE_BOOLEAN, &playing, + DBUS_TYPE_INVALID); + + return reply; +} + +static DBusMessage *hs_disconnect(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + char hs_address[18]; + + if (hs->state == HEADSET_STATE_DISCONNECTED) + return btd_error_not_connected(msg); + + headset_shutdown(device); + ba2str(&device->dst, hs_address); + info("Disconnected from %s, %s", hs_address, device->path); + + return dbus_message_new_method_return(msg); + +} + +static DBusMessage *hs_is_connected(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + DBusMessage *reply; + dbus_bool_t connected; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + connected = (device->headset->state >= HEADSET_STATE_CONNECTED); + + dbus_message_append_args(reply, DBUS_TYPE_BOOLEAN, &connected, + DBUS_TYPE_INVALID); + + return reply; +} + +static DBusMessage *hs_connect(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + int err; + + if (hs->state == HEADSET_STATE_CONNECTING) + return btd_error_in_progress(msg); + else if (hs->state > HEADSET_STATE_CONNECTING) + return btd_error_already_connected(msg); + + if (hs->hfp_handle && !ag.telephony_ready) + return btd_error_not_ready(msg); + + device->auto_connect = FALSE; + + err = rfcomm_connect(device, NULL, NULL, NULL); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + hs->auto_dc = FALSE; + + hs->pending->msg = dbus_message_ref(msg); + + return NULL; +} + +static DBusMessage *hs_ring(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + DBusMessage *reply = NULL; + int err; + + if (hs->state < HEADSET_STATE_CONNECTED) + return btd_error_not_connected(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + if (ag.ring_timer) { + DBG("IndicateCall received when already indicating"); + return reply; + } + + err = headset_send(hs, "\r\nRING\r\n"); + if (err < 0) { + dbus_message_unref(reply); + return btd_error_failed(msg, strerror(-err)); + } + + ring_timer_cb(NULL); + ag.ring_timer = g_timeout_add_seconds(RING_INTERVAL, ring_timer_cb, + NULL); + + return reply; +} + +static DBusMessage *hs_cancel_call(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + DBusMessage *reply = NULL; + + if (hs->state < HEADSET_STATE_CONNECTED) + return btd_error_not_connected(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + if (ag.ring_timer) { + g_source_remove(ag.ring_timer); + ag.ring_timer = 0; + } else + DBG("Got CancelCall method call but no call is active"); + + return reply; +} + +static DBusMessage *hs_play(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + int err; + + if (sco_hci) { + error("Refusing Headset.Play() because SCO HCI routing " + "is enabled"); + return btd_error_not_available(msg); + } + + switch (hs->state) { + case HEADSET_STATE_DISCONNECTED: + case HEADSET_STATE_CONNECTING: + return btd_error_not_connected(msg); + case HEADSET_STATE_PLAY_IN_PROGRESS: + if (hs->pending && hs->pending->msg == NULL) { + hs->pending->msg = dbus_message_ref(msg); + return NULL; + } + return btd_error_busy(msg); + case HEADSET_STATE_PLAYING: + return btd_error_already_connected(msg); + case HEADSET_STATE_CONNECTED: + default: + break; + } + + err = sco_connect(device, NULL, NULL, NULL); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + hs->pending->msg = dbus_message_ref(msg); + + return NULL; +} + +static DBusMessage *hs_get_speaker_gain(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + DBusMessage *reply; + dbus_uint16_t gain; + + if (hs->state < HEADSET_STATE_CONNECTED) + return btd_error_not_available(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + gain = (dbus_uint16_t) slc->sp_gain; + + dbus_message_append_args(reply, DBUS_TYPE_UINT16, &gain, + DBUS_TYPE_INVALID); + + return reply; +} + +static DBusMessage *hs_get_mic_gain(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + DBusMessage *reply; + dbus_uint16_t gain; + + if (hs->state < HEADSET_STATE_CONNECTED || slc == NULL) + return btd_error_not_available(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + gain = (dbus_uint16_t) slc->mic_gain; + + dbus_message_append_args(reply, DBUS_TYPE_UINT16, &gain, + DBUS_TYPE_INVALID); + + return reply; +} + +static DBusMessage *hs_set_gain(DBusConnection *conn, + DBusMessage *msg, + void *data, uint16_t gain, + char type) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + DBusMessage *reply; + int err; + + if (hs->state < HEADSET_STATE_CONNECTED) + return btd_error_not_connected(msg); + + err = headset_set_gain(device, gain, type); + if (err < 0) + return btd_error_invalid_args(msg); + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + if (hs->state == HEADSET_STATE_PLAYING) { + err = headset_send(hs, "\r\n+VG%c=%u\r\n", type, gain); + if (err < 0) { + dbus_message_unref(reply); + return btd_error_failed(msg, strerror(-err)); + } + } + + return reply; +} + +static DBusMessage *hs_set_speaker_gain(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + uint16_t gain; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_UINT16, &gain, + DBUS_TYPE_INVALID)) + return NULL; + + return hs_set_gain(conn, msg, data, gain, HEADSET_GAIN_SPEAKER); +} + +static DBusMessage *hs_set_mic_gain(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + uint16_t gain; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_UINT16, &gain, + DBUS_TYPE_INVALID)) + return NULL; + + return hs_set_gain(conn, msg, data, gain, HEADSET_GAIN_MICROPHONE); +} + +static DBusMessage *hs_get_properties(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + gboolean value; + const char *state; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + + /* Playing */ + value = (device->headset->state == HEADSET_STATE_PLAYING); + dict_append_entry(&dict, "Playing", DBUS_TYPE_BOOLEAN, &value); + + /* State */ + state = state2str(device->headset->state); + if (state) + dict_append_entry(&dict, "State", DBUS_TYPE_STRING, &state); + + /* Connected */ + value = (device->headset->state >= HEADSET_STATE_CONNECTED); + dict_append_entry(&dict, "Connected", DBUS_TYPE_BOOLEAN, &value); + + if (!value) + goto done; + + /* SpeakerGain */ + dict_append_entry(&dict, "SpeakerGain", + DBUS_TYPE_UINT16, + &device->headset->slc->sp_gain); + + /* MicrophoneGain */ + dict_append_entry(&dict, "MicrophoneGain", + DBUS_TYPE_UINT16, + &device->headset->slc->mic_gain); + +done: + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static DBusMessage *hs_set_property(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + const char *property; + DBusMessageIter iter; + DBusMessageIter sub; + uint16_t gain; + + if (!dbus_message_iter_init(msg, &iter)) + return btd_error_invalid_args(msg); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&iter, &property); + dbus_message_iter_next(&iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) + return btd_error_invalid_args(msg); + dbus_message_iter_recurse(&iter, &sub); + + if (g_str_equal("SpeakerGain", property)) { + if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_UINT16) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&sub, &gain); + return hs_set_gain(conn, msg, data, gain, + HEADSET_GAIN_SPEAKER); + } else if (g_str_equal("MicrophoneGain", property)) { + if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_UINT16) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&sub, &gain); + return hs_set_gain(conn, msg, data, gain, + HEADSET_GAIN_MICROPHONE); + } + + return btd_error_invalid_args(msg); +} + +#ifdef __TIZEN_PATCH__ +static DBusMessage *hs_set_voice_dial(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct headset *hs = device->headset; + struct headset_slc *slc = hs->slc; + DBusMessage *reply; + int err; + dbus_bool_t enable; + + if (hs->state < HEADSET_STATE_CONNECTED) + return btd_error_not_connected(msg); + + if (!(slc->hf_features & HF_FEATURE_VOICE_RECOGNITION)) { + DBG("Voice Recognition is not supported by HF \n"); + return btd_error_not_supported(msg); + } + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_BOOLEAN, &enable, + DBUS_TYPE_INVALID)) + return NULL; + + DBG("hs_set_voice_dial = %d \n", enable); + + reply = dbus_message_new_method_return(msg); + + if (!reply) + return NULL; + + err = headset_send(hs, "\r\n+BVRA: %d\r\n", enable); + + if (err < 0) { + dbus_message_unref(reply); + return btd_error_failed(msg, strerror(-err)); + } + + return reply; +} +#endif + +static const GDBusMethodTable headset_methods[] = { + { GDBUS_ASYNC_METHOD("Connect", NULL, NULL, hs_connect) }, + { GDBUS_METHOD("Disconnect", NULL, NULL, hs_disconnect) }, + { GDBUS_METHOD("IsConnected", + NULL, GDBUS_ARGS({ "connected", "b" }), + hs_is_connected) }, + { GDBUS_METHOD("IndicateCall", NULL, NULL, hs_ring) }, + { GDBUS_METHOD("CancelCall", NULL, NULL, hs_cancel_call) }, + { GDBUS_DEPRECATED_ASYNC_METHOD("Play", NULL, NULL, hs_play) }, + { GDBUS_METHOD("Stop", NULL, NULL, hs_stop) }, + { GDBUS_DEPRECATED_METHOD("IsPlaying", + NULL, GDBUS_ARGS({ "playing", "b" }), + hs_is_playing) }, + { GDBUS_DEPRECATED_METHOD("GetSpeakerGain", + NULL, GDBUS_ARGS({ "gain", "q" }), + hs_get_speaker_gain) }, + { GDBUS_DEPRECATED_METHOD("GetMicrophoneGain", + NULL, GDBUS_ARGS({ "gain", "q" }), + hs_get_mic_gain) }, + { GDBUS_DEPRECATED_METHOD("SetSpeakerGain", + GDBUS_ARGS({ "gain", "q" }), NULL, + hs_set_speaker_gain) }, + { GDBUS_DEPRECATED_METHOD("SetMicrophoneGain", + GDBUS_ARGS({ "gain", "q" }), NULL, + hs_set_mic_gain) }, + { GDBUS_METHOD("GetProperties", + NULL, GDBUS_ARGS({ "properties", "a{sv}" }), + hs_get_properties) }, + { GDBUS_METHOD("SetProperty", + GDBUS_ARGS({ "name", "s" }, { "value", "v" }), NULL, + hs_set_property) }, +#ifdef __TIZEN_PATCH__ + { GDBUS_METHOD("SetVoiceDial", + GDBUS_ARGS({ "enable", "b" }), NULL, + hs_set_voice_dial) }, +#endif + { } +}; + +static const GDBusSignalTable headset_signals[] = { + { GDBUS_DEPRECATED_SIGNAL("Connected", NULL) }, + { GDBUS_DEPRECATED_SIGNAL("Disconnected", NULL) }, + { GDBUS_DEPRECATED_SIGNAL("AnswerRequested", NULL) }, + { GDBUS_DEPRECATED_SIGNAL("Stopped", NULL) }, + { GDBUS_DEPRECATED_SIGNAL("Playing", NULL) }, + { GDBUS_DEPRECATED_SIGNAL("SpeakerGainChanged", + GDBUS_ARGS({ "gain", "q" })) }, + { GDBUS_DEPRECATED_SIGNAL("MicrophoneGainChanged", + GDBUS_ARGS({ "gain", "q" })) }, + { GDBUS_SIGNAL("CallTerminated", NULL) }, + { GDBUS_SIGNAL("PropertyChanged", + GDBUS_ARGS({ "name", "s" }, { "value", "v" })) }, + { } +}; + +void headset_update(struct audio_device *dev, uint16_t svc, + const char *uuidstr) +{ + struct headset *headset = dev->headset; + const sdp_record_t *record; + + record = btd_device_get_record(dev->btd_dev, uuidstr); + if (!record) + return; + + switch (svc) { + case HANDSFREE_SVCLASS_ID: + if (headset->hfp_handle && + (headset->hfp_handle != record->handle)) { + error("More than one HFP record found on device"); + return; + } + + headset->hfp_handle = record->handle; + break; + + case HEADSET_SVCLASS_ID: + if (headset->hsp_handle && + (headset->hsp_handle != record->handle)) { + error("More than one HSP record found on device"); + return; + } + + headset->hsp_handle = record->handle; + + /* Ignore this record if we already have access to HFP */ + if (headset->hfp_handle) + return; + + break; + + default: + DBG("Invalid record passed to headset_update"); + return; + } +} + +static int headset_close_rfcomm(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + GIOChannel *rfcomm = hs->tmp_rfcomm ? hs->tmp_rfcomm : hs->rfcomm; + + if (rfcomm) { + g_io_channel_shutdown(rfcomm, TRUE, NULL); + g_io_channel_unref(rfcomm); + hs->tmp_rfcomm = NULL; + hs->rfcomm = NULL; + } + + g_free(hs->slc); + hs->slc = NULL; + + return 0; +} + +static void headset_free(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + if (hs->dc_timer) { + g_source_remove(hs->dc_timer); + hs->dc_timer = 0; + } + + close_sco(dev); + + headset_close_rfcomm(dev); + + g_slist_free_full(hs->nrec_cbs, g_free); + + g_free(hs); + dev->headset = NULL; +} + +static void path_unregister(void *data) +{ + struct audio_device *dev = data; + struct headset *hs = dev->headset; + + if (hs->state > HEADSET_STATE_DISCONNECTED) { + DBG("Headset unregistered while device was connected!"); + headset_shutdown(dev); + } + + DBG("Unregistered interface %s on path %s", + AUDIO_HEADSET_INTERFACE, dev->path); + + headset_free(dev); +} + +void headset_unregister(struct audio_device *dev) +{ + g_dbus_unregister_interface(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE); +} + +struct headset *headset_init(struct audio_device *dev, uint16_t svc, + const char *uuidstr) +{ + struct headset *hs; + const sdp_record_t *record; + + hs = g_new0(struct headset, 1); + hs->rfcomm_ch = -1; + hs->search_hfp = server_is_enabled(&dev->src, HANDSFREE_SVCLASS_ID); + + record = btd_device_get_record(dev->btd_dev, uuidstr); + if (!record) + goto register_iface; + + switch (svc) { + case HANDSFREE_SVCLASS_ID: + hs->hfp_handle = record->handle; + break; + + case HEADSET_SVCLASS_ID: + hs->hsp_handle = record->handle; + break; + + default: + DBG("Invalid record passed to headset_init"); + g_free(hs); + return NULL; + } + +register_iface: + if (!g_dbus_register_interface(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, + headset_methods, headset_signals, NULL, + dev, path_unregister)) { + g_free(hs); + return NULL; + } + + DBG("Registered interface %s on path %s", + AUDIO_HEADSET_INTERFACE, dev->path); + + return hs; +} + +uint32_t headset_config_init(GKeyFile *config) +{ + GError *err = NULL; + char *str; + + /* Use the default values if there is no config file */ + if (config == NULL) + return ag.features; + + str = g_key_file_get_string(config, "General", "SCORouting", + &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + if (strcmp(str, "PCM") == 0) + sco_hci = FALSE; + else if (strcmp(str, "HCI") == 0) + sco_hci = TRUE; + else + error("Invalid Headset Routing value: %s", str); + g_free(str); + } + + /* Init fast connectable option */ + str = g_key_file_get_string(config, "Headset", "FastConnectable", + &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else { + fast_connectable = strcmp(str, "true") == 0; + if (fast_connectable) + manager_set_fast_connectable(FALSE); + g_free(str); + } + + return ag.features; +} + +static gboolean hs_dc_timeout(struct audio_device *dev) +{ + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); + return FALSE; +} + +gboolean headset_cancel_stream(struct audio_device *dev, unsigned int id) +{ + struct headset *hs = dev->headset; + struct pending_connect *p = hs->pending; + GSList *l; + struct connect_cb *cb = NULL; + + if (!p) + return FALSE; + + for (l = p->callbacks; l != NULL; l = l->next) { + struct connect_cb *tmp = l->data; + + if (tmp->id == id) { + cb = tmp; + break; + } + } + + if (!cb) + return FALSE; + + p->callbacks = g_slist_remove(p->callbacks, cb); + g_free(cb); + + if (p->callbacks || p->msg) + return TRUE; + + if (hs->auto_dc) { + if (hs->rfcomm) + hs->dc_timer = g_timeout_add_seconds(DC_TIMEOUT, + (GSourceFunc) hs_dc_timeout, + dev); + else + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); + } + + return TRUE; +} + +static gboolean dummy_connect_complete(struct audio_device *dev) +{ + pending_connect_finalize(dev); + return FALSE; +} + +unsigned int headset_request_stream(struct audio_device *dev, + headset_stream_cb_t cb, + void *user_data) +{ + struct headset *hs = dev->headset; + unsigned int id; + + if (hs->state == HEADSET_STATE_PLAYING) { + id = connect_cb_new(hs, HEADSET_STATE_PLAYING, cb, user_data); + g_idle_add((GSourceFunc) dummy_connect_complete, dev); + return id; + } + + if (hs->dc_timer) { + g_source_remove(hs->dc_timer); + hs->dc_timer = 0; + } + + if (hs->state == HEADSET_STATE_CONNECTING || + hs->state == HEADSET_STATE_PLAY_IN_PROGRESS) + return connect_cb_new(hs, HEADSET_STATE_PLAYING, cb, user_data); + + if (hs->rfcomm == NULL) { + if (rfcomm_connect(dev, cb, user_data, &id) < 0) + return 0; + hs->auto_dc = TRUE; + } else if (sco_connect(dev, cb, user_data, &id) < 0) + return 0; + + hs->pending->target_state = HEADSET_STATE_PLAYING; + + return id; +} + +unsigned int headset_config_stream(struct audio_device *dev, + gboolean auto_dc, + headset_stream_cb_t cb, + void *user_data) +{ + struct headset *hs = dev->headset; + unsigned int id = 0; + + if (hs->dc_timer) { + g_source_remove(hs->dc_timer); + hs->dc_timer = 0; + } + + if (hs->state == HEADSET_STATE_CONNECTING) + return connect_cb_new(hs, HEADSET_STATE_CONNECTED, cb, + user_data); + + if (hs->rfcomm) + goto done; + + if (rfcomm_connect(dev, cb, user_data, &id) < 0) + return 0; + + hs->auto_dc = auto_dc; + hs->pending->target_state = HEADSET_STATE_CONNECTED; + + return id; + +done: + id = connect_cb_new(hs, HEADSET_STATE_CONNECTED, cb, user_data); + g_idle_add((GSourceFunc) dummy_connect_complete, dev); + return id; +} + +unsigned int headset_suspend_stream(struct audio_device *dev, + headset_stream_cb_t cb, + void *user_data) +{ + struct headset *hs = dev->headset; + unsigned int id; + int sock; + + if (hs->state == HEADSET_STATE_DISCONNECTED || + hs->state == HEADSET_STATE_CONNECTING) + return 0; + + if (hs->dc_timer) { + g_source_remove(hs->dc_timer); + hs->dc_timer = 0; + } + + if (hs->sco) { + sock = g_io_channel_unix_get_fd(hs->sco); + + /* shutdown but leave the socket open and wait for hup */ + shutdown(sock, SHUT_RDWR); + } else { + headset_set_state(dev, HEADSET_STATE_CONNECTED); + + g_idle_add((GSourceFunc) dummy_connect_complete, dev); + } + + id = connect_cb_new(hs, HEADSET_STATE_CONNECTED, cb, user_data); + + return id; +} + +gboolean headset_get_hfp_active(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + return hs->hfp_active; +} + +void headset_set_hfp_active(struct audio_device *dev, gboolean active) +{ + struct headset *hs = dev->headset; + + hs->hfp_active = active; +} + +gboolean headset_get_rfcomm_initiator(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + return hs->rfcomm_initiator; +} + +void headset_set_rfcomm_initiator(struct audio_device *dev, + gboolean initiator) +{ + struct headset *hs = dev->headset; + + hs->rfcomm_initiator = initiator; +} + +GIOChannel *headset_get_rfcomm(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + return hs->tmp_rfcomm; +} + +int headset_connect_rfcomm(struct audio_device *dev, GIOChannel *io) +{ + struct headset *hs = dev->headset; + + if (hs->tmp_rfcomm) + return -EALREADY; + + hs->tmp_rfcomm = g_io_channel_ref(io); + + return 0; +} + +int headset_connect_sco(struct audio_device *dev, GIOChannel *io) +{ + struct headset *hs = dev->headset; + struct headset_slc *slc = hs->slc; + + if (hs->sco) + return -EISCONN; + + hs->sco = g_io_channel_ref(io); + + if (slc->pending_ring) { + ring_timer_cb(NULL); + ag.ring_timer = g_timeout_add_seconds(RING_INTERVAL, + ring_timer_cb, + NULL); + slc->pending_ring = FALSE; + } + + return 0; +} + +void headset_set_state(struct audio_device *dev, headset_state_t state) +{ + struct headset *hs = dev->headset; + struct headset_slc *slc = hs->slc; + gboolean value; + const char *state_str; + headset_state_t old_state = hs->state; + GSList *l; + + if (old_state == state) + return; + + state_str = state2str(state); + + switch (state) { + case HEADSET_STATE_DISCONNECTED: + value = FALSE; + close_sco(dev); + headset_close_rfcomm(dev); + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, + "Disconnected", + DBUS_TYPE_INVALID); + if (hs->state > HEADSET_STATE_CONNECTING) { + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "Connected", + DBUS_TYPE_BOOLEAN, &value); + telephony_device_disconnected(dev); + } + active_devices = g_slist_remove(active_devices, dev); + break; + case HEADSET_STATE_CONNECTING: + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); + break; + case HEADSET_STATE_CONNECTED: + close_sco(dev); + if (hs->state != HEADSET_STATE_PLAY_IN_PROGRESS) + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); + if (hs->state < state) { + if (ag.features & AG_FEATURE_INBAND_RINGTONE) + slc->inband_ring = TRUE; + else + slc->inband_ring = FALSE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, + "Connected", + DBUS_TYPE_INVALID); + value = TRUE; + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, + "Connected", + DBUS_TYPE_BOOLEAN, &value); + active_devices = g_slist_append(active_devices, dev); + telephony_device_connected(dev); + } else if (hs->state == HEADSET_STATE_PLAYING) { + value = FALSE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, + "Stopped", + DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, + "Playing", + DBUS_TYPE_BOOLEAN, &value); + } + break; + case HEADSET_STATE_PLAY_IN_PROGRESS: + break; + case HEADSET_STATE_PLAYING: + value = TRUE; + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); + + /* Do not watch HUP since we need to know when the link is + really disconnected */ + hs->sco_id = g_io_add_watch(hs->sco, + G_IO_ERR | G_IO_NVAL, + (GIOFunc) sco_cb, dev); + + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "Playing", + DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_HEADSET_INTERFACE, "Playing", + DBUS_TYPE_BOOLEAN, &value); + + if (slc->sp_gain >= 0) + headset_send(hs, "\r\n+VGS=%u\r\n", slc->sp_gain); + if (slc->mic_gain >= 0) + headset_send(hs, "\r\n+VGM=%u\r\n", slc->mic_gain); + break; + } + + hs->state = state; + + DBG("State changed %s: %s -> %s", dev->path, str_state[old_state], + str_state[state]); + + for (l = headset_callbacks; l != NULL; l = l->next) { + struct headset_state_callback *cb = l->data; + cb->cb(dev, old_state, state, cb->user_data); + } +} + +headset_state_t headset_get_state(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + return hs->state; +} + +int headset_get_channel(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + return hs->rfcomm_ch; +} + +gboolean headset_is_active(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + if (hs->state != HEADSET_STATE_DISCONNECTED) + return TRUE; + + return FALSE; +} + +headset_lock_t headset_get_lock(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + return hs->lock; +} + +gboolean headset_lock(struct audio_device *dev, headset_lock_t lock) +{ + struct headset *hs = dev->headset; + + if (hs->lock & lock) + return FALSE; + + hs->lock |= lock; + + return TRUE; +} + +gboolean headset_unlock(struct audio_device *dev, headset_lock_t lock) +{ + struct headset *hs = dev->headset; + + if (!(hs->lock & lock)) + return FALSE; + + hs->lock &= ~lock; + + if (hs->lock) + return TRUE; + + if (hs->state == HEADSET_STATE_PLAYING) + headset_set_state(dev, HEADSET_STATE_CONNECTED); + + if (hs->auto_dc) { + if (hs->state == HEADSET_STATE_CONNECTED) + hs->dc_timer = g_timeout_add_seconds(DC_TIMEOUT, + (GSourceFunc) hs_dc_timeout, + dev); + else + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); + } + + return TRUE; +} + +gboolean headset_suspend(struct audio_device *dev, void *data) +{ + return TRUE; +} + +gboolean headset_play(struct audio_device *dev, void *data) +{ + return TRUE; +} + +int headset_get_sco_fd(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + if (!hs->sco) + return -1; + + return g_io_channel_unix_get_fd(hs->sco); +} + +gboolean headset_get_nrec(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + if (!hs->slc) + return TRUE; + + return hs->slc->nrec; +} + +unsigned int headset_add_nrec_cb(struct audio_device *dev, + headset_nrec_cb cb, void *user_data) +{ + struct headset *hs = dev->headset; + struct headset_nrec_callback *nrec_cb; + static unsigned int id = 0; + + nrec_cb = g_new(struct headset_nrec_callback, 1); + nrec_cb->cb = cb; + nrec_cb->user_data = user_data; + nrec_cb->id = ++id; + + hs->nrec_cbs = g_slist_prepend(hs->nrec_cbs, nrec_cb); + + return nrec_cb->id; +} + +gboolean headset_remove_nrec_cb(struct audio_device *dev, unsigned int id) +{ + struct headset *hs = dev->headset; + GSList *l; + + for (l = hs->nrec_cbs; l != NULL; l = l->next) { + struct headset_nrec_callback *cb = l->data; + if (cb && cb->id == id) { + hs->nrec_cbs = g_slist_remove(hs->nrec_cbs, cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} + +gboolean headset_get_inband(struct audio_device *dev) +{ + struct headset *hs = dev->headset; + + if (!hs->slc) + return TRUE; + + return hs->slc->inband_ring; +} + +gboolean headset_get_sco_hci(struct audio_device *dev) +{ + return sco_hci; +} + +void headset_shutdown(struct audio_device *dev) +{ + struct pending_connect *p = dev->headset->pending; + + if (p && p->msg) + error_connect_failed(dev->conn, p->msg, ECANCELED); + + pending_connect_finalize(dev); + headset_set_state(dev, HEADSET_STATE_DISCONNECTED); +} + +int telephony_event_ind(int index) +{ + if (!active_devices) + return -ENODEV; + + if (!ag.er_ind) { + DBG("telephony_report_event called but events are disabled"); + return -EINVAL; + } + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CIEV: %d,%d\r\n", index + 1, + ag.indicators[index].val); + + return 0; +} + +int telephony_response_and_hold_ind(int rh) +{ + if (!active_devices) + return -ENODEV; + + ag.rh = rh; + + /* If we aren't in any response and hold state don't send anything */ + if (ag.rh < 0) + return 0; + + send_foreach_headset(active_devices, hfp_cmp, "\r\n+BTRH: %d\r\n", + ag.rh); + + return 0; +} + +int telephony_incoming_call_ind(const char *number, int type) +{ + struct audio_device *dev; + struct headset *hs; + struct headset_slc *slc; + + if (fast_connectable) + manager_set_fast_connectable(TRUE); + + if (!active_devices) + return -ENODEV; + + /* Get the latest connected device */ + dev = active_devices->data; + hs = dev->headset; + slc = hs->slc; + + if (ag.ring_timer) { + DBG("telephony_incoming_call_ind: already calling"); + return -EBUSY; + } + + /* With HSP 1.2 the RING messages should *not* be sent if inband + * ringtone is being used */ + if (!hs->hfp_active && slc->inband_ring) + return 0; + + g_free(ag.number); + ag.number = g_strdup(number); + ag.number_type = type; + + if (slc->inband_ring && hs->hfp_active && + hs->state != HEADSET_STATE_PLAYING) { + slc->pending_ring = TRUE; + return 0; + } + + ring_timer_cb(NULL); + ag.ring_timer = g_timeout_add_seconds(RING_INTERVAL, ring_timer_cb, + NULL); + + return 0; +} + +int telephony_calling_stopped_ind(void) +{ + struct audio_device *dev; + + if (fast_connectable) + manager_set_fast_connectable(FALSE); + + if (ag.ring_timer) { + g_source_remove(ag.ring_timer); + ag.ring_timer = 0; + } + + if (!active_devices) + return 0; + + /* In case SCO isn't fully up yet */ + dev = active_devices->data; + + if (!dev->headset->slc->pending_ring && !ag.ring_timer) + return -EINVAL; + + dev->headset->slc->pending_ring = FALSE; + + return 0; +} + +int telephony_ready_ind(uint32_t features, + const struct indicator *indicators, int rh, + const char *chld) +{ + ag.telephony_ready = TRUE; + ag.features = features; + ag.indicators = indicators; + ag.rh = rh; + ag.chld = chld; + + DBG("Telephony plugin initialized"); + + print_ag_features(ag.features); + + return 0; +} + +int telephony_deinit(void) +{ + g_free(ag.number); + + memset(&ag, 0, sizeof(ag)); + + ag.er_mode = 3; + ag.rh = BTRH_NOT_SUPPORTED; + + DBG("Telephony deinitialized"); + + return 0; +} + +int telephony_list_current_call_ind(int idx, int dir, int status, int mode, + int mprty, const char *number, + int type) +{ + if (!active_devices) + return -ENODEV; + + if (number && strlen(number) > 0) + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CLCC: %d,%d,%d,%d,%d,\"%s\",%d\r\n", + idx, dir, status, mode, mprty, number, type); + else + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CLCC: %d,%d,%d,%d,%d\r\n", + idx, dir, status, mode, mprty); + + return 0; +} + +int telephony_subscriber_number_ind(const char *number, int type, int service) +{ + if (!active_devices) + return -ENODEV; + + send_foreach_headset(active_devices, hfp_cmp, + "\r\n+CNUM: ,%s,%d,,%d\r\n", + number, type, service); + + return 0; +} + +static int cwa_cmp(struct headset *hs) +{ + if (!hs->hfp_active) + return -1; + + if (hs->slc->cwa_enabled) + return 0; + else + return -1; +} + +int telephony_call_waiting_ind(const char *number, int type) +{ + if (!active_devices) + return -ENODEV; + + send_foreach_headset(active_devices, cwa_cmp, + "\r\n+CCWA: \"%s\",%d\r\n", + number, type); + + return 0; +} + +unsigned int headset_add_state_cb(headset_state_cb cb, void *user_data) +{ + struct headset_state_callback *state_cb; + static unsigned int id = 0; + + state_cb = g_new(struct headset_state_callback, 1); + state_cb->cb = cb; + state_cb->user_data = user_data; + state_cb->id = ++id; + + headset_callbacks = g_slist_append(headset_callbacks, state_cb); + + return state_cb->id; +} + +gboolean headset_remove_state_cb(unsigned int id) +{ + GSList *l; + + for (l = headset_callbacks; l != NULL; l = l->next) { + struct headset_state_callback *cb = l->data; + if (cb && cb->id == id) { + headset_callbacks = g_slist_remove(headset_callbacks, cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} diff --git a/audio/headset.h b/audio/headset.h new file mode 100644 index 0000000..465c2d6 --- /dev/null +++ b/audio/headset.h @@ -0,0 +1,112 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define AUDIO_HEADSET_INTERFACE "org.bluez.Headset" + +#define DEFAULT_HS_AG_CHANNEL 12 +#define DEFAULT_HF_AG_CHANNEL 13 + +typedef enum { + HEADSET_STATE_DISCONNECTED, + HEADSET_STATE_CONNECTING, + HEADSET_STATE_CONNECTED, + HEADSET_STATE_PLAY_IN_PROGRESS, + HEADSET_STATE_PLAYING +} headset_state_t; + +typedef enum { + HEADSET_LOCK_READ = 1, + HEADSET_LOCK_WRITE = 1 << 1, +} headset_lock_t; + +typedef void (*headset_state_cb) (struct audio_device *dev, + headset_state_t old_state, + headset_state_t new_state, + void *user_data); +typedef void (*headset_nrec_cb) (struct audio_device *dev, + gboolean nrec, + void *user_data); + +unsigned int headset_add_state_cb(headset_state_cb cb, void *user_data); +gboolean headset_remove_state_cb(unsigned int id); + +typedef void (*headset_stream_cb_t) (struct audio_device *dev, void *user_data); + +void headset_connect_cb(GIOChannel *chan, GError *err, gpointer user_data); + +GIOChannel *headset_get_rfcomm(struct audio_device *dev); + +struct headset *headset_init(struct audio_device *dev, uint16_t svc, + const char *uuidstr); + +void headset_unregister(struct audio_device *dev); + +uint32_t headset_config_init(GKeyFile *config); + +void headset_update(struct audio_device *dev, uint16_t svc, + const char *uuidstr); + +unsigned int headset_config_stream(struct audio_device *dev, + gboolean auto_dc, + headset_stream_cb_t cb, + void *user_data); +unsigned int headset_request_stream(struct audio_device *dev, + headset_stream_cb_t cb, + void *user_data); +unsigned int headset_suspend_stream(struct audio_device *dev, + headset_stream_cb_t cb, + void *user_data); +gboolean headset_cancel_stream(struct audio_device *dev, unsigned int id); + +gboolean headset_get_hfp_active(struct audio_device *dev); +void headset_set_hfp_active(struct audio_device *dev, gboolean active); + +gboolean headset_get_rfcomm_initiator(struct audio_device *dev); +void headset_set_rfcomm_initiator(struct audio_device *dev, + gboolean initiator); + +int headset_connect_rfcomm(struct audio_device *dev, GIOChannel *chan); +int headset_connect_sco(struct audio_device *dev, GIOChannel *io); + +headset_state_t headset_get_state(struct audio_device *dev); +void headset_set_state(struct audio_device *dev, headset_state_t state); + +int headset_get_channel(struct audio_device *dev); + +int headset_get_sco_fd(struct audio_device *dev); +gboolean headset_get_nrec(struct audio_device *dev); +unsigned int headset_add_nrec_cb(struct audio_device *dev, + headset_nrec_cb cb, void *user_data); +gboolean headset_remove_nrec_cb(struct audio_device *dev, unsigned int id); +gboolean headset_get_inband(struct audio_device *dev); +gboolean headset_get_sco_hci(struct audio_device *dev); + +gboolean headset_is_active(struct audio_device *dev); + +headset_lock_t headset_get_lock(struct audio_device *dev); +gboolean headset_lock(struct audio_device *dev, headset_lock_t lock); +gboolean headset_unlock(struct audio_device *dev, headset_lock_t lock); +gboolean headset_suspend(struct audio_device *dev, void *data); +gboolean headset_play(struct audio_device *dev, void *data); +void headset_shutdown(struct audio_device *dev); diff --git a/audio/ipc.c b/audio/ipc.c new file mode 100644 index 0000000..02d956b --- /dev/null +++ b/audio/ipc.c @@ -0,0 +1,134 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "ipc.h" + +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) + +/* This table contains the string representation for messages types */ +static const char *strtypes[] = { + "BT_REQUEST", + "BT_RESPONSE", + "BT_INDICATION", + "BT_ERROR", +}; + +/* This table contains the string representation for messages names */ +static const char *strnames[] = { + "BT_GET_CAPABILITIES", + "BT_OPEN", + "BT_SET_CONFIGURATION", + "BT_NEW_STREAM", + "BT_START_STREAM", + "BT_STOP_STREAM", + "BT_SUSPEND_STREAM", + "BT_RESUME_STREAM", + "BT_CONTROL", +}; + +int bt_audio_service_open(void) +{ + int sk; + int err; + struct sockaddr_un addr = { + AF_UNIX, BT_IPC_SOCKET_NAME + }; + + sk = socket(PF_LOCAL, SOCK_STREAM, 0); + if (sk < 0) { + err = -errno; + fprintf(stderr, "%s: Cannot open socket: %s (%d)\n", + __FUNCTION__, strerror(-err), -err); + errno = -err; + return -1; + } + + if (connect(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + err = -errno; + fprintf(stderr, "%s: connect() failed: %s (%d)\n", + __FUNCTION__, strerror(-err), -err); + close(sk); + errno = -err; + return -1; + } + + return sk; +} + +int bt_audio_service_close(int sk) +{ + return close(sk); +} + +int bt_audio_service_get_data_fd(int sk) +{ + char cmsg_b[CMSG_SPACE(sizeof(int))], m; + int err, ret; + struct iovec iov = { &m, sizeof(m) }; + struct msghdr msgh; + struct cmsghdr *cmsg; + + memset(&msgh, 0, sizeof(msgh)); + msgh.msg_iov = &iov; + msgh.msg_iovlen = 1; + msgh.msg_control = &cmsg_b; + msgh.msg_controllen = CMSG_LEN(sizeof(int)); + + ret = recvmsg(sk, &msgh, 0); + if (ret < 0) { + err = -errno; + fprintf(stderr, "%s: Unable to receive fd: %s (%d)\n", + __FUNCTION__, strerror(-err), -err); + errno = -err; + return -1; + } + + /* Receive auxiliary data in msgh */ + for (cmsg = CMSG_FIRSTHDR(&msgh); cmsg != NULL; + cmsg = CMSG_NXTHDR(&msgh, cmsg)) { + if (cmsg->cmsg_level == SOL_SOCKET + && cmsg->cmsg_type == SCM_RIGHTS) { + memcpy(&ret, CMSG_DATA(cmsg), sizeof(int)); + return ret; + } + } + + errno = EINVAL; + return -1; +} + +const char *bt_audio_strtype(uint8_t type) +{ + if (type >= ARRAY_SIZE(strtypes)) + return NULL; + + return strtypes[type]; +} + +const char *bt_audio_strname(uint8_t name) +{ + if (name >= ARRAY_SIZE(strnames)) + return NULL; + + return strnames[name]; +} diff --git a/audio/ipc.h b/audio/ipc.h new file mode 100644 index 0000000..61ae019 --- /dev/null +++ b/audio/ipc.h @@ -0,0 +1,361 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +/* + Message sequence chart of streaming sequence for A2DP transport + + Audio daemon User + on snd_pcm_open + <--BT_GET_CAPABILITIES_REQ + + BT_GET_CAPABILITIES_RSP--> + + on snd_pcm_hw_params + <--BT_SETCONFIGURATION_REQ + + BT_SET_CONFIGURATION_RSP--> + + on snd_pcm_prepare + <--BT_START_STREAM_REQ + + + BT_START_STREAM_RSP--> + + BT_NEW_STREAM_IND --> + + < streams data > + .......... + + on snd_pcm_drop/snd_pcm_drain + + <--BT_STOP_STREAM_REQ + + + BT_STOP_STREAM_RSP--> + + on IPC close or appl crash + + + */ + +#ifndef BT_AUDIOCLIENT_H +#define BT_AUDIOCLIENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include + +#define BT_SUGGESTED_BUFFER_SIZE 512 +#define BT_IPC_SOCKET_NAME "\0/org/bluez/audio" + +/* Generic message header definition, except for RESPONSE messages */ +typedef struct { + uint8_t type; + uint8_t name; + uint16_t length; +} __attribute__ ((packed)) bt_audio_msg_header_t; + +typedef struct { + bt_audio_msg_header_t h; + uint8_t posix_errno; +} __attribute__ ((packed)) bt_audio_error_t; + +/* Message types */ +#define BT_REQUEST 0 +#define BT_RESPONSE 1 +#define BT_INDICATION 2 +#define BT_ERROR 3 + +/* Messages names */ +#define BT_GET_CAPABILITIES 0 +#define BT_OPEN 1 +#define BT_SET_CONFIGURATION 2 +#define BT_NEW_STREAM 3 +#define BT_START_STREAM 4 +#define BT_STOP_STREAM 5 +#define BT_CLOSE 6 +#define BT_CONTROL 7 +#define BT_DELAY_REPORT 8 + +#define BT_CAPABILITIES_TRANSPORT_A2DP 0 +#define BT_CAPABILITIES_TRANSPORT_SCO 1 +#define BT_CAPABILITIES_TRANSPORT_ANY 2 + +#define BT_CAPABILITIES_ACCESS_MODE_READ 1 +#define BT_CAPABILITIES_ACCESS_MODE_WRITE 2 +#define BT_CAPABILITIES_ACCESS_MODE_READWRITE 3 + +#define BT_FLAG_AUTOCONNECT 1 + +struct bt_get_capabilities_req { + bt_audio_msg_header_t h; + char source[18]; /* Address of the local Device */ + char destination[18];/* Address of the remote Device */ + char object[128]; /* DBus object path */ + uint8_t transport; /* Requested transport */ + uint8_t flags; /* Requested flags */ + uint8_t seid; /* Requested capability configuration */ +} __attribute__ ((packed)); + +/** + * SBC Codec parameters as per A2DP profile 1.0 § 4.3 + */ + +/* A2DP seid are 6 bytes long so HSP/HFP are assigned to 7-8 bits */ +#define BT_A2DP_SEID_RANGE (1 << 6) - 1 + +#define BT_A2DP_SBC_SOURCE 0x00 +#define BT_A2DP_SBC_SINK 0x01 +#define BT_A2DP_MPEG12_SOURCE 0x02 +#define BT_A2DP_MPEG12_SINK 0x03 +#define BT_A2DP_MPEG24_SOURCE 0x04 +#define BT_A2DP_MPEG24_SINK 0x05 +#define BT_A2DP_ATRAC_SOURCE 0x06 +#define BT_A2DP_ATRAC_SINK 0x07 +#define BT_A2DP_UNKNOWN_SOURCE 0x08 +#define BT_A2DP_UNKNOWN_SINK 0x09 + +#define BT_SBC_SAMPLING_FREQ_16000 (1 << 3) +#define BT_SBC_SAMPLING_FREQ_32000 (1 << 2) +#define BT_SBC_SAMPLING_FREQ_44100 (1 << 1) +#define BT_SBC_SAMPLING_FREQ_48000 1 + +#define BT_A2DP_CHANNEL_MODE_MONO (1 << 3) +#define BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL (1 << 2) +#define BT_A2DP_CHANNEL_MODE_STEREO (1 << 1) +#define BT_A2DP_CHANNEL_MODE_JOINT_STEREO 1 + +#define BT_A2DP_BLOCK_LENGTH_4 (1 << 3) +#define BT_A2DP_BLOCK_LENGTH_8 (1 << 2) +#define BT_A2DP_BLOCK_LENGTH_12 (1 << 1) +#define BT_A2DP_BLOCK_LENGTH_16 1 + +#define BT_A2DP_SUBBANDS_4 (1 << 1) +#define BT_A2DP_SUBBANDS_8 1 + +#define BT_A2DP_ALLOCATION_SNR (1 << 1) +#define BT_A2DP_ALLOCATION_LOUDNESS 1 + +#define BT_MPEG_SAMPLING_FREQ_16000 (1 << 5) +#define BT_MPEG_SAMPLING_FREQ_22050 (1 << 4) +#define BT_MPEG_SAMPLING_FREQ_24000 (1 << 3) +#define BT_MPEG_SAMPLING_FREQ_32000 (1 << 2) +#define BT_MPEG_SAMPLING_FREQ_44100 (1 << 1) +#define BT_MPEG_SAMPLING_FREQ_48000 1 + +#define BT_MPEG_LAYER_1 (1 << 2) +#define BT_MPEG_LAYER_2 (1 << 1) +#define BT_MPEG_LAYER_3 1 + +#define BT_HFP_CODEC_PCM 0x00 + +#define BT_PCM_FLAG_NREC 0x01 +#define BT_PCM_FLAG_PCM_ROUTING 0x02 + +#define BT_WRITE_LOCK (1 << 1) +#define BT_READ_LOCK 1 + +typedef struct { + uint8_t seid; + uint8_t transport; + uint8_t type; + uint8_t length; + uint8_t configured; + uint8_t lock; + uint8_t data[0]; +} __attribute__ ((packed)) codec_capabilities_t; + +typedef struct { + codec_capabilities_t capability; + uint8_t channel_mode; + uint8_t frequency; + uint8_t allocation_method; + uint8_t subbands; + uint8_t block_length; + uint8_t min_bitpool; + uint8_t max_bitpool; +} __attribute__ ((packed)) sbc_capabilities_t; + +typedef struct { + codec_capabilities_t capability; + uint8_t channel_mode; + uint8_t crc; + uint8_t layer; + uint8_t frequency; + uint8_t mpf; + uint16_t bitrate; +} __attribute__ ((packed)) mpeg_capabilities_t; + +typedef struct { + codec_capabilities_t capability; + uint8_t flags; + uint16_t sampling_rate; +} __attribute__ ((packed)) pcm_capabilities_t; + +struct bt_get_capabilities_rsp { + bt_audio_msg_header_t h; + char source[18]; /* Address of the local Device */ + char destination[18];/* Address of the remote Device */ + char object[128]; /* DBus object path */ + uint8_t data[0]; /* First codec_capabilities_t */ +} __attribute__ ((packed)); + +struct bt_open_req { + bt_audio_msg_header_t h; + char source[18]; /* Address of the local Device */ + char destination[18];/* Address of the remote Device */ + char object[128]; /* DBus object path */ + uint8_t seid; /* Requested capability configuration to lock */ + uint8_t lock; /* Requested lock */ +} __attribute__ ((packed)); + +struct bt_open_rsp { + bt_audio_msg_header_t h; + char source[18]; /* Address of the local Device */ + char destination[18];/* Address of the remote Device */ + char object[128]; /* DBus object path */ +} __attribute__ ((packed)); + +struct bt_set_configuration_req { + bt_audio_msg_header_t h; + codec_capabilities_t codec; /* Requested codec */ +} __attribute__ ((packed)); + +struct bt_set_configuration_rsp { + bt_audio_msg_header_t h; + uint16_t link_mtu; /* Max length that transport supports */ +} __attribute__ ((packed)); + +#define BT_STREAM_ACCESS_READ 0 +#define BT_STREAM_ACCESS_WRITE 1 +#define BT_STREAM_ACCESS_READWRITE 2 +struct bt_start_stream_req { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_start_stream_rsp { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +/* This message is followed by one byte of data containing the stream data fd + as ancillary data */ +struct bt_new_stream_ind { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_stop_stream_req { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_stop_stream_rsp { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_close_req { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_close_rsp { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_suspend_stream_ind { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +struct bt_resume_stream_ind { + bt_audio_msg_header_t h; +} __attribute__ ((packed)); + +#define BT_CONTROL_KEY_POWER 0x40 +#define BT_CONTROL_KEY_VOL_UP 0x41 +#define BT_CONTROL_KEY_VOL_DOWN 0x42 +#define BT_CONTROL_KEY_MUTE 0x43 +#define BT_CONTROL_KEY_PLAY 0x44 +#define BT_CONTROL_KEY_STOP 0x45 +#define BT_CONTROL_KEY_PAUSE 0x46 +#define BT_CONTROL_KEY_RECORD 0x47 +#define BT_CONTROL_KEY_REWIND 0x48 +#define BT_CONTROL_KEY_FAST_FORWARD 0x49 +#define BT_CONTROL_KEY_EJECT 0x4A +#define BT_CONTROL_KEY_FORWARD 0x4B +#define BT_CONTROL_KEY_BACKWARD 0x4C + +struct bt_control_req { + bt_audio_msg_header_t h; + uint8_t mode; /* Control Mode */ + uint8_t key; /* Control Key */ +} __attribute__ ((packed)); + +struct bt_control_rsp { + bt_audio_msg_header_t h; + uint8_t mode; /* Control Mode */ + uint8_t key; /* Control Key */ +} __attribute__ ((packed)); + +struct bt_control_ind { + bt_audio_msg_header_t h; + uint8_t mode; /* Control Mode */ + uint8_t key; /* Control Key */ +} __attribute__ ((packed)); + +struct bt_delay_report_req { + bt_audio_msg_header_t h; + uint16_t delay; +} __attribute__ ((packed)); + +struct bt_delay_report_ind { + bt_audio_msg_header_t h; + uint16_t delay; +} __attribute__ ((packed)); + +/* Function declaration */ + +/* Opens a connection to the audio service: return a socket descriptor */ +int bt_audio_service_open(void); + +/* Closes a connection to the audio service */ +int bt_audio_service_close(int sk); + +/* Receives stream data file descriptor : must be called after a +BT_STREAMFD_IND message is returned */ +int bt_audio_service_get_data_fd(int sk); + +/* Human readable message type string */ +const char *bt_audio_strtype(uint8_t type); + +/* Human readable message name string */ +const char *bt_audio_strname(uint8_t name); + +#ifdef __cplusplus +} +#endif + +#endif /* BT_AUDIOCLIENT_H */ diff --git a/audio/main.c b/audio/main.c new file mode 100644 index 0000000..5c751af --- /dev/null +++ b/audio/main.c @@ -0,0 +1,194 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "glib-helper.h" +#include "btio.h" +#include "plugin.h" +#include "log.h" +#include "device.h" +#include "headset.h" +#include "manager.h" +#include "gateway.h" + +static GIOChannel *sco_server = NULL; + +static GKeyFile *load_config_file(const char *file) +{ + GError *err = NULL; + GKeyFile *keyfile; + + keyfile = g_key_file_new(); + + g_key_file_set_list_separator(keyfile, ','); + + if (!g_key_file_load_from_file(keyfile, file, 0, &err)) { + error("Parsing %s failed: %s", file, err->message); + g_error_free(err); + g_key_file_free(keyfile); + return NULL; + } + + return keyfile; +} + +static void sco_server_cb(GIOChannel *chan, GError *err, gpointer data) +{ + int sk; + struct audio_device *device; + char addr[18]; + bdaddr_t src, dst; + + if (err) { + error("sco_server_cb: %s", err->message); + return; + } + + bt_io_get(chan, BT_IO_SCO, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_DEST_BDADDR, &dst, + BT_IO_OPT_DEST, addr, + BT_IO_OPT_INVALID); + if (err) { + error("bt_io_get: %s", err->message); + goto drop; + } + + device = manager_find_device(NULL, &src, &dst, AUDIO_HEADSET_INTERFACE, + FALSE); + if (!device) + device = manager_find_device(NULL, &src, &dst, + AUDIO_GATEWAY_INTERFACE, + FALSE); + + if (!device) + goto drop; + + if (device->headset) { + if (headset_get_state(device) < HEADSET_STATE_CONNECTED) { + DBG("Refusing SCO from non-connected headset"); + goto drop; + } + + if (!headset_get_hfp_active(device)) { + error("Refusing non-HFP SCO connect attempt from %s", + addr); + goto drop; + } + + if (headset_connect_sco(device, chan) < 0) + goto drop; + + headset_set_state(device, HEADSET_STATE_PLAYING); + } else if (device->gateway) { + if (!gateway_is_connected(device)) { + DBG("Refusing SCO from non-connected AG"); + goto drop; + } + + if (gateway_connect_sco(device, chan) < 0) + goto drop; + } else + goto drop; + + sk = g_io_channel_unix_get_fd(chan); + fcntl(sk, F_SETFL, 0); + + DBG("Accepted SCO connection from %s", addr); + + return; + +drop: + g_io_channel_shutdown(chan, TRUE, NULL); +} + +static DBusConnection *connection; + +static int audio_init(void) +{ + GKeyFile *config; + gboolean enable_sco; + + connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + if (connection == NULL) + return -EIO; + + config = load_config_file(CONFIGDIR "/audio.conf"); + + if (audio_manager_init(connection, config, &enable_sco) < 0) + goto failed; + + if (!enable_sco) + return 0; + + sco_server = bt_io_listen(BT_IO_SCO, sco_server_cb, NULL, NULL, + NULL, NULL, + BT_IO_OPT_INVALID); + if (!sco_server) { + error("Unable to start SCO server socket"); + goto failed; + } + + return 0; + +failed: + audio_manager_exit(); + + if (connection) { + dbus_connection_unref(connection); + connection = NULL; + } + + return -EIO; +} + +static void audio_exit(void) +{ + if (sco_server) { + g_io_channel_shutdown(sco_server, TRUE, NULL); + g_io_channel_unref(sco_server); + sco_server = NULL; + } + + audio_manager_exit(); + + dbus_connection_unref(connection); +} + +BLUETOOTH_PLUGIN_DEFINE(audio, VERSION, + BLUETOOTH_PLUGIN_PRIORITY_DEFAULT, audio_init, audio_exit) diff --git a/audio/manager.c b/audio/manager.c new file mode 100644 index 0000000..946a7df --- /dev/null +++ b/audio/manager.c @@ -0,0 +1,1473 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "glib-helper.h" +#include "btio.h" +#include "../src/adapter.h" +#include "../src/manager.h" +#include "../src/device.h" + +#include "log.h" +#include "ipc.h" +#include "device.h" +#include "error.h" +#include "avdtp.h" +#include "media.h" +#include "a2dp.h" +#include "headset.h" +#include "gateway.h" +#include "sink.h" +#include "source.h" +#include "avrcp.h" +#include "control.h" +#include "manager.h" +#include "sdpd.h" +#include "telephony.h" +#include "unix.h" + +typedef enum { + HEADSET = 1 << 0, + GATEWAY = 1 << 1, + SINK = 1 << 2, + SOURCE = 1 << 3, + CONTROL = 1 << 4, + TARGET = 1 << 5, + INVALID = 1 << 6 +} audio_service_type; + +typedef enum { + GENERIC_AUDIO = 0, + ADVANCED_AUDIO, + AV_REMOTE, + GET_RECORDS +} audio_sdp_state_t; + +struct audio_adapter { + struct btd_adapter *btd_adapter; + gboolean powered; + uint32_t hsp_ag_record_id; + uint32_t hfp_ag_record_id; + uint32_t hfp_hs_record_id; + GIOChannel *hsp_ag_server; + GIOChannel *hfp_ag_server; + GIOChannel *hfp_hs_server; + gint ref; +}; + +static gboolean auto_connect = TRUE; +static int max_connected_headsets = 1; +static DBusConnection *connection = NULL; +static GKeyFile *config = NULL; +static GSList *adapters = NULL; +static GSList *devices = NULL; + +static struct enabled_interfaces enabled = { + .hfp = TRUE, + .headset = TRUE, + .gateway = FALSE, + .sink = TRUE, + .source = FALSE, + .control = TRUE, + .socket = FALSE, + .media = TRUE, +}; + +static struct audio_adapter *find_adapter(GSList *list, + struct btd_adapter *btd_adapter) +{ + for (; list; list = list->next) { + struct audio_adapter *adapter = list->data; + + if (adapter->btd_adapter == btd_adapter) + return adapter; + } + + return NULL; +} + +gboolean server_is_enabled(bdaddr_t *src, uint16_t svc) +{ + switch (svc) { + case HEADSET_SVCLASS_ID: + return enabled.headset; + case HEADSET_AGW_SVCLASS_ID: + return FALSE; + case HANDSFREE_SVCLASS_ID: + return enabled.headset && enabled.hfp; + case HANDSFREE_AGW_SVCLASS_ID: + return enabled.gateway; + case AUDIO_SINK_SVCLASS_ID: + return enabled.sink; + case AUDIO_SOURCE_SVCLASS_ID: + return enabled.source; + case AV_REMOTE_TARGET_SVCLASS_ID: + case AV_REMOTE_SVCLASS_ID: + return enabled.control; + default: + return FALSE; + } +} + +static void handle_uuid(const char *uuidstr, struct audio_device *device) +{ + uuid_t uuid; + uint16_t uuid16; + + if (bt_string2uuid(&uuid, uuidstr) < 0) { + error("%s not detected as an UUID-128", uuidstr); + return; + } + + if (!sdp_uuid128_to_uuid(&uuid) && uuid.type != SDP_UUID16) { + error("Could not convert %s to a UUID-16", uuidstr); + return; + } + + uuid16 = uuid.value.uuid16; + + if (!server_is_enabled(&device->src, uuid16)) { + DBG("server not enabled for %s (0x%04x)", uuidstr, uuid16); + return; + } + + switch (uuid16) { + case HEADSET_SVCLASS_ID: + DBG("Found Headset record"); + if (device->headset) + headset_update(device, uuid16, uuidstr); + else + device->headset = headset_init(device, uuid16, + uuidstr); + break; + case HEADSET_AGW_SVCLASS_ID: + DBG("Found Headset AG record"); + break; + case HANDSFREE_SVCLASS_ID: + DBG("Found Handsfree record"); + if (device->headset) + headset_update(device, uuid16, uuidstr); + else + device->headset = headset_init(device, uuid16, + uuidstr); + break; + case HANDSFREE_AGW_SVCLASS_ID: + DBG("Found Handsfree AG record"); + if (enabled.gateway && (device->gateway == NULL)) + device->gateway = gateway_init(device); + break; + case AUDIO_SINK_SVCLASS_ID: + DBG("Found Audio Sink"); + if (device->sink == NULL) + device->sink = sink_init(device); + break; + case AUDIO_SOURCE_SVCLASS_ID: + DBG("Found Audio Source"); + if (device->source == NULL) + device->source = source_init(device); + break; + case AV_REMOTE_SVCLASS_ID: + case AV_REMOTE_TARGET_SVCLASS_ID: + DBG("Found AV %s", uuid16 == AV_REMOTE_SVCLASS_ID ? + "Remote" : "Target"); + if (device->control) + control_update(device->control, uuid16); + else + device->control = control_init(device, uuid16); + + if (device->sink && sink_is_active(device)) + avrcp_connect(device); + break; + default: + DBG("Unrecognized UUID: 0x%04X", uuid16); + break; + } +} + +static sdp_record_t *hsp_ag_record(uint8_t ch) +{ + sdp_list_t *svclass_id, *pfseq, *apseq, *root; + uuid_t root_uuid, svclass_uuid, ga_svclass_uuid; + uuid_t l2cap_uuid, rfcomm_uuid; + sdp_profile_desc_t profile; + sdp_record_t *record; + sdp_list_t *aproto, *proto[2]; + sdp_data_t *channel; + + record = sdp_record_alloc(); + if (!record) + return NULL; + + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups(record, root); + + sdp_uuid16_create(&svclass_uuid, HEADSET_AGW_SVCLASS_ID); + svclass_id = sdp_list_append(0, &svclass_uuid); + sdp_uuid16_create(&ga_svclass_uuid, GENERIC_AUDIO_SVCLASS_ID); + svclass_id = sdp_list_append(svclass_id, &ga_svclass_uuid); + sdp_set_service_classes(record, svclass_id); + + sdp_uuid16_create(&profile.uuid, HEADSET_PROFILE_ID); + profile.version = 0x0102; + pfseq = sdp_list_append(0, &profile); + sdp_set_profile_descs(record, pfseq); + + sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID); + proto[0] = sdp_list_append(0, &l2cap_uuid); + apseq = sdp_list_append(0, proto[0]); + + sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID); + proto[1] = sdp_list_append(0, &rfcomm_uuid); + channel = sdp_data_alloc(SDP_UINT8, &ch); + proto[1] = sdp_list_append(proto[1], channel); + apseq = sdp_list_append(apseq, proto[1]); + + aproto = sdp_list_append(0, apseq); + sdp_set_access_protos(record, aproto); + + sdp_set_info_attr(record, "Headset Audio Gateway", 0, 0); + + sdp_data_free(channel); + sdp_list_free(proto[0], 0); + sdp_list_free(proto[1], 0); + sdp_list_free(apseq, 0); + sdp_list_free(pfseq, 0); + sdp_list_free(aproto, 0); + sdp_list_free(root, 0); + sdp_list_free(svclass_id, 0); + + return record; +} + +static sdp_record_t *hfp_hs_record(uint8_t ch) +{ + sdp_list_t *svclass_id, *pfseq, *apseq, *root; + uuid_t root_uuid, svclass_uuid, ga_svclass_uuid; + uuid_t l2cap_uuid, rfcomm_uuid; + sdp_profile_desc_t profile; + sdp_record_t *record; + sdp_list_t *aproto, *proto[2]; + sdp_data_t *channel; + + record = sdp_record_alloc(); + if (!record) + return NULL; + + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups(record, root); + + sdp_uuid16_create(&svclass_uuid, HANDSFREE_SVCLASS_ID); + svclass_id = sdp_list_append(0, &svclass_uuid); + sdp_uuid16_create(&ga_svclass_uuid, GENERIC_AUDIO_SVCLASS_ID); + svclass_id = sdp_list_append(svclass_id, &ga_svclass_uuid); + sdp_set_service_classes(record, svclass_id); + + sdp_uuid16_create(&profile.uuid, HANDSFREE_PROFILE_ID); + profile.version = 0x0105; + pfseq = sdp_list_append(0, &profile); + sdp_set_profile_descs(record, pfseq); + + sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID); + proto[0] = sdp_list_append(0, &l2cap_uuid); + apseq = sdp_list_append(0, proto[0]); + + sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID); + proto[1] = sdp_list_append(0, &rfcomm_uuid); + channel = sdp_data_alloc(SDP_UINT8, &ch); + proto[1] = sdp_list_append(proto[1], channel); + apseq = sdp_list_append(apseq, proto[1]); + + aproto = sdp_list_append(0, apseq); + sdp_set_access_protos(record, aproto); + + sdp_set_info_attr(record, "Hands-Free", 0, 0); + + sdp_data_free(channel); + sdp_list_free(proto[0], 0); + sdp_list_free(proto[1], 0); + sdp_list_free(apseq, 0); + sdp_list_free(pfseq, 0); + sdp_list_free(aproto, 0); + sdp_list_free(root, 0); + sdp_list_free(svclass_id, 0); + + return record; +} + +static sdp_record_t *hfp_ag_record(uint8_t ch, uint32_t feat) +{ + sdp_list_t *svclass_id, *pfseq, *apseq, *root; + uuid_t root_uuid, svclass_uuid, ga_svclass_uuid; + uuid_t l2cap_uuid, rfcomm_uuid; + sdp_profile_desc_t profile; + sdp_list_t *aproto, *proto[2]; + sdp_record_t *record; + sdp_data_t *channel, *features; + uint8_t netid = 0x01; + uint16_t sdpfeat; + sdp_data_t *network; + + record = sdp_record_alloc(); + if (!record) + return NULL; + + network = sdp_data_alloc(SDP_UINT8, &netid); + if (!network) { + sdp_record_free(record); + return NULL; + } + + sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); + root = sdp_list_append(0, &root_uuid); + sdp_set_browse_groups(record, root); + + sdp_uuid16_create(&svclass_uuid, HANDSFREE_AGW_SVCLASS_ID); + svclass_id = sdp_list_append(0, &svclass_uuid); + sdp_uuid16_create(&ga_svclass_uuid, GENERIC_AUDIO_SVCLASS_ID); + svclass_id = sdp_list_append(svclass_id, &ga_svclass_uuid); + sdp_set_service_classes(record, svclass_id); + + sdp_uuid16_create(&profile.uuid, HANDSFREE_PROFILE_ID); + profile.version = 0x0105; + pfseq = sdp_list_append(0, &profile); + sdp_set_profile_descs(record, pfseq); + + sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID); + proto[0] = sdp_list_append(0, &l2cap_uuid); + apseq = sdp_list_append(0, proto[0]); + + sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID); + proto[1] = sdp_list_append(0, &rfcomm_uuid); + channel = sdp_data_alloc(SDP_UINT8, &ch); + proto[1] = sdp_list_append(proto[1], channel); + apseq = sdp_list_append(apseq, proto[1]); + + sdpfeat = (uint16_t) feat & 0xF; + features = sdp_data_alloc(SDP_UINT16, &sdpfeat); + sdp_attr_add(record, SDP_ATTR_SUPPORTED_FEATURES, features); + + aproto = sdp_list_append(0, apseq); + sdp_set_access_protos(record, aproto); + + sdp_set_info_attr(record, "Hands-Free Audio Gateway", 0, 0); + + sdp_attr_add(record, SDP_ATTR_EXTERNAL_NETWORK, network); + + sdp_data_free(channel); + sdp_list_free(proto[0], 0); + sdp_list_free(proto[1], 0); + sdp_list_free(apseq, 0); + sdp_list_free(pfseq, 0); + sdp_list_free(aproto, 0); + sdp_list_free(root, 0); + sdp_list_free(svclass_id, 0); + + return record; +} + +static void headset_auth_cb(DBusError *derr, void *user_data) +{ + struct audio_device *device = user_data; + GError *err = NULL; + GIOChannel *io; + + if (device->hs_preauth_id) { + g_source_remove(device->hs_preauth_id); + device->hs_preauth_id = 0; + } + + if (derr && dbus_error_is_set(derr)) { + error("Access denied: %s", derr->message); + headset_set_state(device, HEADSET_STATE_DISCONNECTED); + return; + } + + io = headset_get_rfcomm(device); + + if (!bt_io_accept(io, headset_connect_cb, device, NULL, &err)) { + error("bt_io_accept: %s", err->message); + g_error_free(err); + headset_set_state(device, HEADSET_STATE_DISCONNECTED); + return; + } +} + +static gboolean hs_preauth_cb(GIOChannel *chan, GIOCondition cond, + gpointer user_data) +{ + struct audio_device *device = user_data; + + DBG("Headset disconnected during authorization"); + + audio_device_cancel_authorization(device, headset_auth_cb, device); + + headset_set_state(device, HEADSET_STATE_DISCONNECTED); + + device->hs_preauth_id = 0; + + return FALSE; +} + +static void ag_confirm(GIOChannel *chan, gpointer data) +{ + const char *server_uuid, *remote_uuid; + struct audio_device *device; + gboolean hfp_active; + bdaddr_t src, dst; + int perr; + GError *err = NULL; + uint8_t ch; + + bt_io_get(chan, BT_IO_RFCOMM, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_DEST_BDADDR, &dst, + BT_IO_OPT_CHANNEL, &ch, + BT_IO_OPT_INVALID); + if (err) { + error("%s", err->message); + g_error_free(err); + goto drop; + } + + if (ch == DEFAULT_HS_AG_CHANNEL) { + hfp_active = FALSE; + server_uuid = HSP_AG_UUID; + remote_uuid = HSP_HS_UUID; + } else { + hfp_active = TRUE; + server_uuid = HFP_AG_UUID; + remote_uuid = HFP_HS_UUID; + } + + device = manager_get_device(&src, &dst, TRUE); + if (!device) + goto drop; + + if (!manager_allow_headset_connection(device)) { + DBG("Refusing headset: too many existing connections"); + goto drop; + } + + if (!device->headset) { + btd_device_add_uuid(device->btd_dev, remote_uuid); + if (!device->headset) + goto drop; + } + + if (headset_get_state(device) > HEADSET_STATE_DISCONNECTED) { + DBG("Refusing new connection since one already exists"); + goto drop; + } + + headset_set_hfp_active(device, hfp_active); + headset_set_rfcomm_initiator(device, TRUE); + + if (headset_connect_rfcomm(device, chan) < 0) { + error("headset_connect_rfcomm failed"); + goto drop; + } + + headset_set_state(device, HEADSET_STATE_CONNECTING); + + perr = audio_device_request_authorization(device, server_uuid, + headset_auth_cb, device); + if (perr < 0) { + DBG("Authorization denied: %s", strerror(-perr)); + headset_set_state(device, HEADSET_STATE_DISCONNECTED); + return; + } + + device->hs_preauth_id = g_io_add_watch(chan, + G_IO_NVAL | G_IO_HUP | G_IO_ERR, + hs_preauth_cb, device); + + device->auto_connect = auto_connect; + + return; + +drop: + g_io_channel_shutdown(chan, TRUE, NULL); +} + +static void gateway_auth_cb(DBusError *derr, void *user_data) +{ + struct audio_device *device = user_data; + + if (derr && dbus_error_is_set(derr)) { + error("Access denied: %s", derr->message); + gateway_set_state(device, GATEWAY_STATE_DISCONNECTED); + } else { + char ag_address[18]; + + ba2str(&device->dst, ag_address); + DBG("Accepted AG connection from %s for %s", + ag_address, device->path); + + gateway_start_service(device); + } +} + +static void hf_io_cb(GIOChannel *chan, gpointer data) +{ + bdaddr_t src, dst; + GError *err = NULL; + uint8_t ch; + const char *server_uuid, *remote_uuid; + struct audio_device *device; + int perr; + + bt_io_get(chan, BT_IO_RFCOMM, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_DEST_BDADDR, &dst, + BT_IO_OPT_CHANNEL, &ch, + BT_IO_OPT_INVALID); + + if (err) { + error("%s", err->message); + g_error_free(err); + return; + } + + server_uuid = HFP_HS_UUID; + remote_uuid = HFP_AG_UUID; + + device = manager_get_device(&src, &dst, TRUE); + if (!device) + goto drop; + + if (!device->gateway) { + btd_device_add_uuid(device->btd_dev, remote_uuid); + if (!device->gateway) + goto drop; + } + + if (gateway_is_active(device)) { + DBG("Refusing new connection since one already exists"); + goto drop; + } + + if (gateway_connect_rfcomm(device, chan) < 0) { + error("Allocating new GIOChannel failed!"); + goto drop; + } + + perr = audio_device_request_authorization(device, server_uuid, + gateway_auth_cb, device); + if (perr < 0) { + DBG("Authorization denied: %s", strerror(-perr)); + gateway_set_state(device, GATEWAY_STATE_DISCONNECTED); + } + + return; + +drop: + g_io_channel_shutdown(chan, TRUE, NULL); +} + +static int headset_server_init(struct audio_adapter *adapter) +{ + uint8_t chan = DEFAULT_HS_AG_CHANNEL; + sdp_record_t *record; + gboolean master = TRUE; + GError *err = NULL; + uint32_t features; + GIOChannel *io; + bdaddr_t src; + + if (config) { + gboolean tmp; + + tmp = g_key_file_get_boolean(config, "General", "Master", + &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else + master = tmp; + } + + adapter_get_address(adapter->btd_adapter, &src); + + io = bt_io_listen(BT_IO_RFCOMM, NULL, ag_confirm, adapter, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_CHANNEL, chan, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_MASTER, master, + BT_IO_OPT_INVALID); + if (!io) + goto failed; + + adapter->hsp_ag_server = io; + + record = hsp_ag_record(chan); + if (!record) { + error("Unable to allocate new service record"); + goto failed; + } + + if (add_record_to_server(&src, record) < 0) { + error("Unable to register HS AG service record"); + sdp_record_free(record); + goto failed; + } + adapter->hsp_ag_record_id = record->handle; + + features = headset_config_init(config); + + if (!enabled.hfp) + return 0; + + chan = DEFAULT_HF_AG_CHANNEL; + + io = bt_io_listen(BT_IO_RFCOMM, NULL, ag_confirm, adapter, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_CHANNEL, chan, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_MASTER, master, + BT_IO_OPT_INVALID); + if (!io) + goto failed; + + adapter->hfp_ag_server = io; + + record = hfp_ag_record(chan, features); + if (!record) { + error("Unable to allocate new service record"); + goto failed; + } + + if (add_record_to_server(&src, record) < 0) { + error("Unable to register HF AG service record"); + sdp_record_free(record); + goto failed; + } + adapter->hfp_ag_record_id = record->handle; + + return 0; + +failed: + if (err) { + error("%s", err->message); + g_error_free(err); + } + + if (adapter->hsp_ag_server) { + g_io_channel_shutdown(adapter->hsp_ag_server, TRUE, NULL); + g_io_channel_unref(adapter->hsp_ag_server); + adapter->hsp_ag_server = NULL; + } + + if (adapter->hfp_ag_server) { + g_io_channel_shutdown(adapter->hfp_ag_server, TRUE, NULL); + g_io_channel_unref(adapter->hfp_ag_server); + adapter->hfp_ag_server = NULL; + } + + return -1; +} + +static int gateway_server_init(struct audio_adapter *adapter) +{ + uint8_t chan = DEFAULT_HFP_HS_CHANNEL; + sdp_record_t *record; + gboolean master = TRUE; + GError *err = NULL; + GIOChannel *io; + bdaddr_t src; + + if (config) { + gboolean tmp; + + tmp = g_key_file_get_boolean(config, "General", "Master", + &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else + master = tmp; + } + + adapter_get_address(adapter->btd_adapter, &src); + + io = bt_io_listen(BT_IO_RFCOMM, NULL, hf_io_cb, adapter, NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, &src, + BT_IO_OPT_CHANNEL, chan, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_MASTER, master, + BT_IO_OPT_INVALID); + if (!io) { + error("%s", err->message); + g_error_free(err); + return -1; + } + + adapter->hfp_hs_server = io; + record = hfp_hs_record(chan); + if (!record) { + error("Unable to allocate new service record"); + goto failed; + } + + if (add_record_to_server(&src, record) < 0) { + error("Unable to register HFP HS service record"); + sdp_record_free(record); + goto failed; + } + + adapter->hfp_hs_record_id = record->handle; + + return 0; + +failed: + g_io_channel_shutdown(adapter->hfp_hs_server, TRUE, NULL); + g_io_channel_unref(adapter->hfp_hs_server); + adapter->hfp_hs_server = NULL; + return -1; +} + +static int audio_probe(struct btd_device *device, GSList *uuids) +{ + struct btd_adapter *adapter = device_get_adapter(device); + bdaddr_t src, dst; + struct audio_device *audio_dev; + + adapter_get_address(adapter, &src); + device_get_address(device, &dst, NULL); + + audio_dev = manager_get_device(&src, &dst, TRUE); + if (!audio_dev) { + DBG("unable to get a device object"); + return -1; + } + + g_slist_foreach(uuids, (GFunc) handle_uuid, audio_dev); + + return 0; +} + +static void audio_remove(struct btd_device *device) +{ + struct audio_device *dev; + const char *path; + + path = device_get_path(device); + + dev = manager_find_device(path, NULL, NULL, NULL, FALSE); + if (!dev) + return; + + devices = g_slist_remove(devices, dev); + + audio_device_unregister(dev); + +} + +static struct audio_adapter *audio_adapter_ref(struct audio_adapter *adp) +{ + adp->ref++; + + DBG("%p: ref=%d", adp, adp->ref); + + return adp; +} + +static void audio_adapter_unref(struct audio_adapter *adp) +{ + adp->ref--; + + DBG("%p: ref=%d", adp, adp->ref); + + if (adp->ref > 0) + return; + + adapters = g_slist_remove(adapters, adp); + btd_adapter_unref(adp->btd_adapter); + g_free(adp); +} + +static struct audio_adapter *audio_adapter_create(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + + adp = g_new0(struct audio_adapter, 1); + adp->btd_adapter = btd_adapter_ref(adapter); + + return audio_adapter_ref(adp); +} + +static struct audio_adapter *audio_adapter_get(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + + adp = find_adapter(adapters, adapter); + if (!adp) { + adp = audio_adapter_create(adapter); + adapters = g_slist_append(adapters, adp); + } else + audio_adapter_ref(adp); + + return adp; +} + +static void state_changed(struct btd_adapter *adapter, gboolean powered) +{ + struct audio_adapter *adp; +#ifndef __TIZEN_PATCH__ + static gboolean telephony = FALSE; +#endif + GSList *l; + + DBG("%s powered %s", adapter_get_path(adapter), + powered ? "on" : "off"); + + /* ignore powered change, adapter is powering down */ + if (powered && adapter_powering_down(adapter)) + return; + + adp = find_adapter(adapters, adapter); + if (!adp) + return; + + adp->powered = powered; + +#ifndef __TIZEN_PATCH__ + if (powered) { + /* telephony driver already initialized*/ + if (telephony == TRUE) + return; + + telephony_init(); + telephony = TRUE; + return; + } + + /* telephony not initialized just ignore power down */ + if (telephony == FALSE) + return; +#endif + + for (l = adapters; l; l = l->next) { + adp = l->data; + + if (adp->powered == TRUE) + return; + } + +#ifndef __TIZEN_PATCH__ + telephony_exit(); + telephony = FALSE; +#endif +} + +static int headset_server_probe(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + int err; + + DBG("path %s", path); + + adp = audio_adapter_get(adapter); + if (!adp) + return -EINVAL; + +#ifdef __TIZEN_PATCH__ + /*telephony driver initialization*/ + if(telephony_init()) + DBG("telephony_init failed\n"); +#endif + + err = headset_server_init(adp); + if (err < 0) { + audio_adapter_unref(adp); + return err; + } + + btd_adapter_register_powered_callback(adapter, state_changed); + + return 0; +} + +static void headset_server_remove(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + + DBG("path %s", path); + +#ifdef __TIZEN_PATCH__ + telephony_exit(); +#endif + + btd_adapter_unregister_powered_callback(adapter, state_changed); + + adp = find_adapter(adapters, adapter); + if (!adp) + return; + + if (adp->hsp_ag_record_id) { + remove_record_from_server(adp->hsp_ag_record_id); + adp->hsp_ag_record_id = 0; + } + + if (adp->hsp_ag_server) { + g_io_channel_shutdown(adp->hsp_ag_server, TRUE, NULL); + g_io_channel_unref(adp->hsp_ag_server); + adp->hsp_ag_server = NULL; + } + + if (adp->hfp_ag_record_id) { + remove_record_from_server(adp->hfp_ag_record_id); + adp->hfp_ag_record_id = 0; + } + + if (adp->hfp_ag_server) { + g_io_channel_shutdown(adp->hfp_ag_server, TRUE, NULL); + g_io_channel_unref(adp->hfp_ag_server); + adp->hfp_ag_server = NULL; + } + + audio_adapter_unref(adp); +} + +static int gateway_server_probe(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + int err; + + adp = audio_adapter_get(adapter); + if (!adp) + return -EINVAL; + + err = gateway_server_init(adp); + if (err < 0) + audio_adapter_unref(adp); + + return err; +} + +static void gateway_server_remove(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + + DBG("path %s", path); + + adp = find_adapter(adapters, adapter); + if (!adp) + return; + + if (adp->hfp_hs_record_id) { + remove_record_from_server(adp->hfp_hs_record_id); + adp->hfp_hs_record_id = 0; + } + + if (adp->hfp_hs_server) { + g_io_channel_shutdown(adp->hfp_hs_server, TRUE, NULL); + g_io_channel_unref(adp->hfp_hs_server); + adp->hfp_hs_server = NULL; + } + + audio_adapter_unref(adp); +} + +static int a2dp_server_probe(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + bdaddr_t src; + int err; + + DBG("path %s", path); + + adp = audio_adapter_get(adapter); + if (!adp) + return -EINVAL; + + adapter_get_address(adapter, &src); + + err = a2dp_register(connection, &src, config); + if (err < 0) + audio_adapter_unref(adp); + + return err; +} + +static void a2dp_server_remove(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + bdaddr_t src; + + DBG("path %s", path); + + adp = find_adapter(adapters, adapter); + if (!adp) + return; + + adapter_get_address(adapter, &src); + a2dp_unregister(&src); + audio_adapter_unref(adp); +} + +static int avrcp_server_probe(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + bdaddr_t src; + int err; + + DBG("path %s", path); + + adp = audio_adapter_get(adapter); + if (!adp) + return -EINVAL; + + adapter_get_address(adapter, &src); + + err = avrcp_register(connection, &src, config); + if (err < 0) + audio_adapter_unref(adp); + + return err; +} + +static void avrcp_server_remove(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + bdaddr_t src; + + DBG("path %s", path); + + adp = find_adapter(adapters, adapter); + if (!adp) + return; + + adapter_get_address(adapter, &src); + avrcp_unregister(&src); + audio_adapter_unref(adp); +} + +static int media_server_probe(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + bdaddr_t src; + int err; + + DBG("path %s", path); + + adp = audio_adapter_get(adapter); + if (!adp) + return -EINVAL; + + adapter_get_address(adapter, &src); + + err = media_register(connection, path, &src); + if (err < 0) + audio_adapter_unref(adp); + + return err; +} + +static void media_server_remove(struct btd_adapter *adapter) +{ + struct audio_adapter *adp; + const gchar *path = adapter_get_path(adapter); + + DBG("path %s", path); + + adp = find_adapter(adapters, adapter); + if (!adp) + return; + + media_unregister(path); + audio_adapter_unref(adp); +} + +static struct btd_device_driver audio_driver = { + .name = "audio", + .uuids = BTD_UUIDS(HSP_HS_UUID, HFP_HS_UUID, HSP_AG_UUID, HFP_AG_UUID, + ADVANCED_AUDIO_UUID, A2DP_SOURCE_UUID, A2DP_SINK_UUID, + AVRCP_TARGET_UUID, AVRCP_REMOTE_UUID), + .probe = audio_probe, + .remove = audio_remove, +}; + +static struct btd_adapter_driver headset_server_driver = { + .name = "audio-headset", + .probe = headset_server_probe, + .remove = headset_server_remove, +}; + +static struct btd_adapter_driver gateway_server_driver = { + .name = "audio-gateway", + .probe = gateway_server_probe, + .remove = gateway_server_remove, +}; + +static struct btd_adapter_driver a2dp_server_driver = { + .name = "audio-a2dp", + .probe = a2dp_server_probe, + .remove = a2dp_server_remove, +}; + +static struct btd_adapter_driver avrcp_server_driver = { + .name = "audio-control", + .probe = avrcp_server_probe, + .remove = avrcp_server_remove, +}; + +static struct btd_adapter_driver media_server_driver = { + .name = "media", + .probe = media_server_probe, + .remove = media_server_remove, +}; + +int audio_manager_init(DBusConnection *conn, GKeyFile *conf, + gboolean *enable_sco) +{ + char **list; + int i; + gboolean b; + GError *err = NULL; + + connection = dbus_connection_ref(conn); + + if (!conf) + goto proceed; + + config = conf; + + list = g_key_file_get_string_list(config, "General", "Enable", + NULL, NULL); + for (i = 0; list && list[i] != NULL; i++) { + if (g_str_equal(list[i], "Headset")) + enabled.headset = TRUE; + else if (g_str_equal(list[i], "Gateway")) + enabled.gateway = TRUE; + else if (g_str_equal(list[i], "Sink")) + enabled.sink = TRUE; + else if (g_str_equal(list[i], "Source")) + enabled.source = TRUE; + else if (g_str_equal(list[i], "Control")) + enabled.control = TRUE; + else if (g_str_equal(list[i], "Socket")) + enabled.socket = TRUE; + else if (g_str_equal(list[i], "Media")) + enabled.media = TRUE; + + } + g_strfreev(list); + + list = g_key_file_get_string_list(config, "General", "Disable", + NULL, NULL); + for (i = 0; list && list[i] != NULL; i++) { + if (g_str_equal(list[i], "Headset")) + enabled.headset = FALSE; + else if (g_str_equal(list[i], "Gateway")) + enabled.gateway = FALSE; + else if (g_str_equal(list[i], "Sink")) + enabled.sink = FALSE; + else if (g_str_equal(list[i], "Source")) + enabled.source = FALSE; + else if (g_str_equal(list[i], "Control")) + enabled.control = FALSE; + else if (g_str_equal(list[i], "Socket")) + enabled.socket = FALSE; + else if (g_str_equal(list[i], "Media")) + enabled.media = FALSE; + } + g_strfreev(list); + + b = g_key_file_get_boolean(config, "General", "AutoConnect", &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else + auto_connect = b; + + b = g_key_file_get_boolean(config, "Headset", "HFP", + &err); + if (err) + g_clear_error(&err); + else + enabled.hfp = b; + + err = NULL; + i = g_key_file_get_integer(config, "Headset", "MaxConnected", + &err); + if (err) { + DBG("audio.conf: %s", err->message); + g_clear_error(&err); + } else + max_connected_headsets = i; + +proceed: + if (enabled.socket) + unix_init(); + + if (enabled.media) + btd_register_adapter_driver(&media_server_driver); + + if (enabled.headset) + btd_register_adapter_driver(&headset_server_driver); + + if (enabled.gateway) + btd_register_adapter_driver(&gateway_server_driver); + + if (enabled.source || enabled.sink) + btd_register_adapter_driver(&a2dp_server_driver); + + if (enabled.control) + btd_register_adapter_driver(&avrcp_server_driver); + + btd_register_device_driver(&audio_driver); + + *enable_sco = (enabled.gateway || enabled.headset); + + return 0; +} + +void audio_manager_exit(void) +{ + /* Bail out early if we haven't been initialized */ + if (connection == NULL) + return; + + dbus_connection_unref(connection); + connection = NULL; + + if (config) { + g_key_file_free(config); + config = NULL; + } + + if (enabled.socket) + unix_exit(); + + if (enabled.media) + btd_unregister_adapter_driver(&media_server_driver); + + if (enabled.headset) + btd_unregister_adapter_driver(&headset_server_driver); + + if (enabled.gateway) + btd_unregister_adapter_driver(&gateway_server_driver); + + if (enabled.source || enabled.sink) + btd_unregister_adapter_driver(&a2dp_server_driver); + + if (enabled.control) + btd_unregister_adapter_driver(&avrcp_server_driver); + + btd_unregister_device_driver(&audio_driver); +} + +GSList *manager_find_devices(const char *path, + const bdaddr_t *src, + const bdaddr_t *dst, + const char *interface, + gboolean connected) +{ + GSList *result = NULL; + GSList *l; + + for (l = devices; l != NULL; l = l->next) { + struct audio_device *dev = l->data; + + if ((path && (strcmp(path, "")) && strcmp(dev->path, path))) + continue; + + if ((src && bacmp(src, BDADDR_ANY)) && bacmp(&dev->src, src)) + continue; + + if ((dst && bacmp(dst, BDADDR_ANY)) && bacmp(&dev->dst, dst)) + continue; + + if (interface && !strcmp(AUDIO_HEADSET_INTERFACE, interface) + && !dev->headset) + continue; + + if (interface && !strcmp(AUDIO_GATEWAY_INTERFACE, interface) + && !dev->gateway) + continue; + + if (interface && !strcmp(AUDIO_SINK_INTERFACE, interface) + && !dev->sink) + continue; + + if (interface && !strcmp(AUDIO_SOURCE_INTERFACE, interface) + && !dev->source) + continue; + + if (interface && !strcmp(AUDIO_CONTROL_INTERFACE, interface) + && !dev->control) + continue; + + if (connected && !audio_device_is_active(dev, interface)) + continue; + + result = g_slist_append(result, dev); + } + + return result; +} + +struct audio_device *manager_find_device(const char *path, + const bdaddr_t *src, + const bdaddr_t *dst, + const char *interface, + gboolean connected) +{ + struct audio_device *result; + GSList *l; + + l = manager_find_devices(path, src, dst, interface, connected); + if (l == NULL) + return NULL; + + result = l->data; + g_slist_free(l); + return result; +} + +struct audio_device *manager_get_device(const bdaddr_t *src, + const bdaddr_t *dst, + gboolean create) +{ + struct audio_device *dev; + struct btd_adapter *adapter; + struct btd_device *device; + char addr[18]; + const char *path; + + dev = manager_find_device(NULL, src, dst, NULL, FALSE); + if (dev) + return dev; + + if (!create) + return NULL; + + ba2str(src, addr); + + adapter = manager_find_adapter(src); + if (!adapter) { + error("Unable to get a btd_adapter object for %s", + addr); + return NULL; + } + + ba2str(dst, addr); + + device = adapter_get_device(connection, adapter, addr); + if (!device) { + error("Unable to get btd_device object for %s", addr); + return NULL; + } + + path = device_get_path(device); + + dev = audio_device_register(connection, device, path, src, dst); + if (!dev) + return NULL; + + devices = g_slist_append(devices, dev); + + return dev; +} + +gboolean manager_allow_headset_connection(struct audio_device *device) +{ + GSList *l; + int connected = 0; + + for (l = devices; l != NULL; l = l->next) { + struct audio_device *dev = l->data; + struct headset *hs = dev->headset; + + if (dev == device) + continue; + + if (device && bacmp(&dev->src, &device->src) != 0) + continue; + + if (!hs) + continue; + + if (headset_get_state(dev) > HEADSET_STATE_DISCONNECTED) + connected++; + + if (connected >= max_connected_headsets) + return FALSE; + } + + return TRUE; +} + +void manager_set_fast_connectable(gboolean enable) +{ + GSList *l; + + if (enable && !manager_allow_headset_connection(NULL)) { + DBG("Refusing enabling fast connectable"); + return; + } + + for (l = adapters; l != NULL; l = l->next) { + struct audio_adapter *adapter = l->data; + + if (btd_adapter_set_fast_connectable(adapter->btd_adapter, + enable)) + error("Changing fast connectable for hci%d failed", + adapter_get_dev_id(adapter->btd_adapter)); + } +} diff --git a/audio/manager.h b/audio/manager.h new file mode 100644 index 0000000..f1d3021 --- /dev/null +++ b/audio/manager.h @@ -0,0 +1,63 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +struct enabled_interfaces { + gboolean hfp; + gboolean headset; + gboolean gateway; + gboolean sink; + gboolean source; + gboolean control; + gboolean socket; + gboolean media; + gboolean media_player; +}; + +int audio_manager_init(DBusConnection *conn, GKeyFile *config, + gboolean *enable_sco); +void audio_manager_exit(void); + +gboolean server_is_enabled(bdaddr_t *src, uint16_t svc); + +struct audio_device *manager_find_device(const char *path, + const bdaddr_t *src, + const bdaddr_t *dst, + const char *interface, + gboolean connected); + +GSList *manager_find_devices(const char *path, + const bdaddr_t *src, + const bdaddr_t *dst, + const char *interface, + gboolean connected); + +struct audio_device *manager_get_device(const bdaddr_t *src, + const bdaddr_t *dst, + gboolean create); + +gboolean manager_allow_headset_connection(struct audio_device *device); + +/* TRUE to enable fast connectable and FALSE to disable fast connectable for all + * audio adapters. */ +void manager_set_fast_connectable(gboolean enable); diff --git a/audio/media.c b/audio/media.c new file mode 100644 index 0000000..1956653 --- /dev/null +++ b/audio/media.c @@ -0,0 +1,1904 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2007 Nokia Corporation + * Copyright (C) 2004-2009 Marcel Holtmann + * Copyright (C) 2011 BMW Car IT GmbH. All rights reserved. + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include +#include + +#include "../src/adapter.h" +#include "../src/dbus-common.h" + +#include "glib-helper.h" +#include "log.h" +#include "error.h" +#include "device.h" +#include "avdtp.h" +#include "media.h" +#include "transport.h" +#include "a2dp.h" +#include "avrcp.h" +#include "headset.h" +#include "gateway.h" +#include "manager.h" + +#define MEDIA_INTERFACE "org.bluez.Media" +#define MEDIA_ENDPOINT_INTERFACE "org.bluez.MediaEndpoint" +#define MEDIA_PLAYER_INTERFACE "org.bluez.MediaPlayer" + +#define REQUEST_TIMEOUT (3 * 1000) /* 3 seconds */ + +struct media_adapter { + bdaddr_t src; /* Adapter address */ + char *path; /* Adapter path */ + DBusConnection *conn; /* Adapter connection */ + GSList *endpoints; /* Endpoints list */ + GSList *players; /* Players list */ +}; + +struct endpoint_request { + struct media_endpoint *endpoint; + DBusMessage *msg; + DBusPendingCall *call; + media_endpoint_cb_t cb; + GDestroyNotify destroy; + void *user_data; +}; + +struct media_endpoint { + struct a2dp_sep *sep; + char *sender; /* Endpoint DBus bus id */ + char *path; /* Endpoint object path */ + char *uuid; /* Endpoint property UUID */ + uint8_t codec; /* Endpoint codec */ + uint8_t *capabilities; /* Endpoint property capabilities */ + size_t size; /* Endpoint capabilities size */ + guint hs_watch; + guint ag_watch; + guint watch; + GSList *requests; + struct media_adapter *adapter; + GSList *transports; +}; + +struct media_player { + struct media_adapter *adapter; + struct avrcp_player *player; + char *sender; /* Player DBus bus id */ + char *path; /* Player object path */ + GHashTable *settings; /* Player settings */ + GHashTable *track; /* Player current track */ + guint watch; + guint property_watch; + guint track_watch; + uint8_t status; + uint32_t position; + uint8_t volume; + GTimer *timer; +}; + +struct metadata_value { + int type; + union { + char *str; + uint32_t num; + } value; +}; + +static GSList *adapters = NULL; + +static void endpoint_request_free(struct endpoint_request *request) +{ + if (request->call) + dbus_pending_call_unref(request->call); + + if (request->destroy) + request->destroy(request->user_data); + + dbus_message_unref(request->msg); + g_free(request); +} + +static void media_endpoint_cancel(struct endpoint_request *request) +{ + struct media_endpoint *endpoint = request->endpoint; + + if (request->call) + dbus_pending_call_cancel(request->call); + + endpoint->requests = g_slist_remove(endpoint->requests, request); + + endpoint_request_free(request); +} + +static void media_endpoint_cancel_all(struct media_endpoint *endpoint) +{ + while (endpoint->requests != NULL) + media_endpoint_cancel(endpoint->requests->data); +} + +static void media_endpoint_destroy(struct media_endpoint *endpoint) +{ + struct media_adapter *adapter = endpoint->adapter; + + DBG("sender=%s path=%s", endpoint->sender, endpoint->path); + + if (endpoint->hs_watch) + headset_remove_state_cb(endpoint->hs_watch); + + if (endpoint->ag_watch) + gateway_remove_state_cb(endpoint->ag_watch); + + media_endpoint_cancel_all(endpoint); + + g_slist_free_full(endpoint->transports, + (GDestroyNotify) media_transport_destroy); + + g_dbus_remove_watch(adapter->conn, endpoint->watch); + g_free(endpoint->capabilities); + g_free(endpoint->sender); + g_free(endpoint->path); + g_free(endpoint->uuid); + g_free(endpoint); +} + +static void media_endpoint_remove(struct media_endpoint *endpoint) +{ + struct media_adapter *adapter = endpoint->adapter; + + if (endpoint->sep) { + a2dp_remove_sep(endpoint->sep); + return; + } + + info("Endpoint unregistered: sender=%s path=%s", endpoint->sender, + endpoint->path); + + adapter->endpoints = g_slist_remove(adapter->endpoints, endpoint); + + media_endpoint_destroy(endpoint); +} + +static void media_endpoint_exit(DBusConnection *connection, void *user_data) +{ + struct media_endpoint *endpoint = user_data; + + endpoint->watch = 0; + media_endpoint_remove(endpoint); +} + +static void headset_setconf_cb(struct media_endpoint *endpoint, void *ret, + int size, void *user_data) +{ + struct audio_device *dev = user_data; + + if (ret != NULL) + return; + + headset_shutdown(dev); +} + +static void clear_configuration(struct media_endpoint *endpoint, + struct media_transport *transport) +{ + DBusConnection *conn; + DBusMessage *msg; + const char *path; + + conn = endpoint->adapter->conn; + + msg = dbus_message_new_method_call(endpoint->sender, endpoint->path, + MEDIA_ENDPOINT_INTERFACE, + "ClearConfiguration"); + if (msg == NULL) { + error("Couldn't allocate D-Bus message"); + goto done; + } + + path = media_transport_get_path(transport); + dbus_message_append_args(msg, DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID); + g_dbus_send_message(conn, msg); +done: + endpoint->transports = g_slist_remove(endpoint->transports, transport); + media_transport_destroy(transport); +} + +static void clear_endpoint(struct media_endpoint *endpoint) +{ + media_endpoint_cancel_all(endpoint); + + while (endpoint->transports != NULL) + clear_configuration(endpoint, endpoint->transports->data); +} + +static void endpoint_reply(DBusPendingCall *call, void *user_data) +{ + struct endpoint_request *request = user_data; + struct media_endpoint *endpoint = request->endpoint; + DBusMessage *reply; + DBusError err; + gboolean value; + void *ret = NULL; + int size = -1; + + /* steal_reply will always return non-NULL since the callback + * is only called after a reply has been received */ + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("Endpoint replied with an error: %s", + err.name); + + /* Clear endpoint configuration in case of NO_REPLY error */ + if (dbus_error_has_name(&err, DBUS_ERROR_NO_REPLY)) { + if (request->cb) + request->cb(endpoint, NULL, size, + request->user_data); + clear_endpoint(endpoint); + dbus_message_unref(reply); + dbus_error_free(&err); + return; + } + + dbus_error_free(&err); + goto done; + } + + if (dbus_message_is_method_call(request->msg, MEDIA_ENDPOINT_INTERFACE, + "SelectConfiguration")) { + DBusMessageIter args, array; + uint8_t *configuration; + + dbus_message_iter_init(reply, &args); + + dbus_message_iter_recurse(&args, &array); + + dbus_message_iter_get_fixed_array(&array, &configuration, &size); + + ret = configuration; + goto done; + } else if (!dbus_message_get_args(reply, &err, DBUS_TYPE_INVALID)) { + error("Wrong reply signature: %s", err.message); + dbus_error_free(&err); + goto done; + } + + size = 1; + value = TRUE; + ret = &value; + +done: + dbus_message_unref(reply); + + if (request->cb) + request->cb(endpoint, ret, size, request->user_data); + + endpoint->requests = g_slist_remove(endpoint->requests, request); + endpoint_request_free(request); +} + +static gboolean media_endpoint_async_call(DBusConnection *conn, + DBusMessage *msg, + struct media_endpoint *endpoint, + media_endpoint_cb_t cb, + void *user_data, + GDestroyNotify destroy) +{ + struct endpoint_request *request; + + request = g_new0(struct endpoint_request, 1); + + /* Timeout should be less than avdtp request timeout (4 seconds) */ + if (dbus_connection_send_with_reply(conn, msg, &request->call, + REQUEST_TIMEOUT) == FALSE) { + error("D-Bus send failed"); + g_free(request); + return FALSE; + } + + dbus_pending_call_set_notify(request->call, endpoint_reply, request, + NULL); + + request->endpoint = endpoint; + request->msg = msg; + request->cb = cb; + request->destroy = destroy; + request->user_data = user_data; + + endpoint->requests = g_slist_append(endpoint->requests, request); + + DBG("Calling %s: name = %s path = %s", dbus_message_get_member(msg), + dbus_message_get_destination(msg), + dbus_message_get_path(msg)); + + return TRUE; +} + +static gboolean select_configuration(struct media_endpoint *endpoint, + uint8_t *capabilities, + size_t length, + media_endpoint_cb_t cb, + void *user_data, + GDestroyNotify destroy) +{ + DBusConnection *conn; + DBusMessage *msg; + + conn = endpoint->adapter->conn; + + msg = dbus_message_new_method_call(endpoint->sender, endpoint->path, + MEDIA_ENDPOINT_INTERFACE, + "SelectConfiguration"); + if (msg == NULL) { + error("Couldn't allocate D-Bus message"); + return FALSE; + } + + dbus_message_append_args(msg, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, + &capabilities, length, + DBUS_TYPE_INVALID); + + return media_endpoint_async_call(conn, msg, endpoint, cb, user_data, + destroy); +} + +static gint transport_device_cmp(gconstpointer data, gconstpointer user_data) +{ + struct media_transport *transport = (struct media_transport *) data; + const struct audio_device *device = user_data; + + if (device == media_transport_get_dev(transport)) + return 0; + + return -1; +} + +static struct media_transport *find_device_transport( + struct media_endpoint *endpoint, + struct audio_device *device) +{ + GSList *match; + + match = g_slist_find_custom(endpoint->transports, device, + transport_device_cmp); + if (match == NULL) + return NULL; + + return match->data; +} + +static gboolean set_configuration(struct media_endpoint *endpoint, + struct audio_device *device, + uint8_t *configuration, size_t size, + media_endpoint_cb_t cb, + void *user_data, + GDestroyNotify destroy) +{ + DBusConnection *conn; + DBusMessage *msg; + const char *path; + DBusMessageIter iter; + struct media_transport *transport; + + transport = find_device_transport(endpoint, device); + + if (transport != NULL) + return FALSE; + + conn = endpoint->adapter->conn; + + transport = media_transport_create(conn, endpoint, device, + configuration, size); + if (transport == NULL) + return FALSE; + + msg = dbus_message_new_method_call(endpoint->sender, endpoint->path, + MEDIA_ENDPOINT_INTERFACE, + "SetConfiguration"); + if (msg == NULL) { + error("Couldn't allocate D-Bus message"); + media_transport_destroy(transport); + return FALSE; + } + + endpoint->transports = g_slist_append(endpoint->transports, transport); + + dbus_message_iter_init_append(msg, &iter); + + path = media_transport_get_path(transport); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &path); + + transport_get_properties(transport, &iter); + + return media_endpoint_async_call(conn, msg, endpoint, cb, user_data, + destroy); +} + +static void release_endpoint(struct media_endpoint *endpoint) +{ + DBusMessage *msg; + + DBG("sender=%s path=%s", endpoint->sender, endpoint->path); + + /* already exit */ + if (endpoint->watch == 0) + goto done; + + msg = dbus_message_new_method_call(endpoint->sender, endpoint->path, + MEDIA_ENDPOINT_INTERFACE, + "Release"); + if (msg == NULL) { + error("Couldn't allocate D-Bus message"); + return; + } + + g_dbus_send_message(endpoint->adapter->conn, msg); + +done: + media_endpoint_remove(endpoint); +} + +static void headset_state_changed(struct audio_device *dev, + headset_state_t old_state, + headset_state_t new_state, + void *user_data) +{ + struct media_endpoint *endpoint = user_data; + struct media_transport *transport; + + DBG(""); + + if (bacmp(&endpoint->adapter->src, &dev->src) != 0) + return; + + switch (new_state) { + case HEADSET_STATE_DISCONNECTED: + transport = find_device_transport(endpoint, dev); + + if (transport != NULL) { + DBG("Clear endpoint %p", endpoint); + clear_configuration(endpoint, transport); + } + break; + case HEADSET_STATE_CONNECTING: + set_configuration(endpoint, dev, NULL, 0, headset_setconf_cb, + dev, NULL); + break; + case HEADSET_STATE_CONNECTED: + break; + case HEADSET_STATE_PLAY_IN_PROGRESS: + break; + case HEADSET_STATE_PLAYING: + break; + } +} + +static const char *get_name(struct a2dp_sep *sep, void *user_data) +{ + struct media_endpoint *endpoint = user_data; + + return endpoint->sender; +} + +static size_t get_capabilities(struct a2dp_sep *sep, uint8_t **capabilities, + void *user_data) +{ + struct media_endpoint *endpoint = user_data; + + *capabilities = endpoint->capabilities; + return endpoint->size; +} + +struct a2dp_config_data { + struct a2dp_setup *setup; + a2dp_endpoint_config_t cb; +}; + +struct a2dp_select_data { + struct a2dp_setup *setup; + a2dp_endpoint_select_t cb; +}; + +static void select_cb(struct media_endpoint *endpoint, void *ret, int size, + void *user_data) +{ + struct a2dp_select_data *data = user_data; + + data->cb(data->setup, ret, size); +} + +static int select_config(struct a2dp_sep *sep, uint8_t *capabilities, + size_t length, struct a2dp_setup *setup, + a2dp_endpoint_select_t cb, void *user_data) +{ + struct media_endpoint *endpoint = user_data; + struct a2dp_select_data *data; + + data = g_new0(struct a2dp_select_data, 1); + data->setup = setup; + data->cb = cb; + + if (select_configuration(endpoint, capabilities, length, + select_cb, data, g_free) == TRUE) + return 0; + + g_free(data); + return -ENOMEM; +} + +static void config_cb(struct media_endpoint *endpoint, void *ret, int size, + void *user_data) +{ + struct a2dp_config_data *data = user_data; + + data->cb(data->setup, ret ? TRUE : FALSE); +} + +static int set_config(struct a2dp_sep *sep, struct audio_device *dev, + uint8_t *configuration, size_t length, + struct a2dp_setup *setup, + a2dp_endpoint_config_t cb, + void *user_data) +{ + struct media_endpoint *endpoint = user_data; + struct a2dp_config_data *data; + + data = g_new0(struct a2dp_config_data, 1); + data->setup = setup; + data->cb = cb; + + if (set_configuration(endpoint, dev, configuration, length, + config_cb, data, g_free) == TRUE) + return 0; + + g_free(data); + return -ENOMEM; +} + +static void clear_config(struct a2dp_sep *sep, void *user_data) +{ + struct media_endpoint *endpoint = user_data; + + clear_endpoint(endpoint); +} + +static void set_delay(struct a2dp_sep *sep, uint16_t delay, void *user_data) +{ + struct media_endpoint *endpoint = user_data; + + if (endpoint->transports == NULL) + return; + + media_transport_update_delay(endpoint->transports->data, delay); +} + +static struct a2dp_endpoint a2dp_endpoint = { + .get_name = get_name, + .get_capabilities = get_capabilities, + .select_configuration = select_config, + .set_configuration = set_config, + .clear_configuration = clear_config, + .set_delay = set_delay +}; + +static void a2dp_destroy_endpoint(void *user_data) +{ + struct media_endpoint *endpoint = user_data; + + clear_endpoint(endpoint); + + endpoint->sep = NULL; + release_endpoint(endpoint); +} + +static void gateway_setconf_cb(struct media_endpoint *endpoint, void *ret, + int size, void *user_data) +{ + struct audio_device *dev = user_data; + + if (ret != NULL) + return; + + gateway_set_state(dev, GATEWAY_STATE_DISCONNECTED); +} + +static void gateway_state_changed(struct audio_device *dev, + gateway_state_t old_state, + gateway_state_t new_state, + void *user_data) +{ + struct media_endpoint *endpoint = user_data; + struct media_transport *transport; + + DBG(""); + + if (bacmp(&endpoint->adapter->src, &dev->src) != 0) + return; + + switch (new_state) { + case GATEWAY_STATE_DISCONNECTED: + transport = find_device_transport(endpoint, dev); + if (transport != NULL) { + DBG("Clear endpoint %p", endpoint); + clear_configuration(endpoint, transport); + } + break; + case GATEWAY_STATE_CONNECTING: + set_configuration(endpoint, dev, NULL, 0, + gateway_setconf_cb, dev, NULL); + break; + case GATEWAY_STATE_CONNECTED: + break; + case GATEWAY_STATE_PLAYING: + break; + } +} + +static gboolean endpoint_init_a2dp_source(struct media_endpoint *endpoint, + gboolean delay_reporting, + int *err) +{ + endpoint->sep = a2dp_add_sep(&endpoint->adapter->src, + AVDTP_SEP_TYPE_SOURCE, endpoint->codec, + delay_reporting, &a2dp_endpoint, + endpoint, a2dp_destroy_endpoint, err); + if (endpoint->sep == NULL) + return FALSE; + + return TRUE; +} + +static gboolean endpoint_init_a2dp_sink(struct media_endpoint *endpoint, + gboolean delay_reporting, + int *err) +{ + endpoint->sep = a2dp_add_sep(&endpoint->adapter->src, + AVDTP_SEP_TYPE_SINK, endpoint->codec, + delay_reporting, &a2dp_endpoint, + endpoint, a2dp_destroy_endpoint, err); + if (endpoint->sep == NULL) + return FALSE; + + return TRUE; +} + +static gboolean endpoint_init_ag(struct media_endpoint *endpoint, int *err) +{ + GSList *list; + GSList *l; + + endpoint->hs_watch = headset_add_state_cb(headset_state_changed, + endpoint); + list = manager_find_devices(NULL, &endpoint->adapter->src, BDADDR_ANY, + AUDIO_HEADSET_INTERFACE, TRUE); + + for (l = list; l != NULL; l = l->next) { + struct audio_device *dev = l->data; + + set_configuration(endpoint, dev, NULL, 0, + headset_setconf_cb, dev, NULL); + } + + g_slist_free(list); + + return TRUE; +} + +static gboolean endpoint_init_hs(struct media_endpoint *endpoint, int *err) +{ + GSList *list; + GSList *l; + + endpoint->ag_watch = gateway_add_state_cb(gateway_state_changed, + endpoint); + list = manager_find_devices(NULL, &endpoint->adapter->src, BDADDR_ANY, + AUDIO_GATEWAY_INTERFACE, TRUE); + + for (l = list; l != NULL; l = l->next) { + struct audio_device *dev = l->data; + + set_configuration(endpoint, dev, NULL, 0, + gateway_setconf_cb, dev, NULL); + } + + g_slist_free(list); + + return TRUE; +} + +static struct media_endpoint *media_endpoint_create(struct media_adapter *adapter, + const char *sender, + const char *path, + const char *uuid, + gboolean delay_reporting, + uint8_t codec, + uint8_t *capabilities, + int size, + int *err) +{ + struct media_endpoint *endpoint; + gboolean succeeded; + + endpoint = g_new0(struct media_endpoint, 1); + endpoint->sender = g_strdup(sender); + endpoint->path = g_strdup(path); + endpoint->uuid = g_strdup(uuid); + endpoint->codec = codec; + + if (size > 0) { + endpoint->capabilities = g_new(uint8_t, size); + memcpy(endpoint->capabilities, capabilities, size); + endpoint->size = size; + } + + endpoint->adapter = adapter; + + if (strcasecmp(uuid, A2DP_SOURCE_UUID) == 0) + succeeded = endpoint_init_a2dp_source(endpoint, + delay_reporting, err); + else if (strcasecmp(uuid, A2DP_SINK_UUID) == 0) + succeeded = endpoint_init_a2dp_sink(endpoint, + delay_reporting, err); + else if (strcasecmp(uuid, HFP_AG_UUID) == 0 || + strcasecmp(uuid, HSP_AG_UUID) == 0) + succeeded = endpoint_init_ag(endpoint, err); + else if (strcasecmp(uuid, HFP_HS_UUID) == 0 || + strcasecmp(uuid, HSP_HS_UUID) == 0) + succeeded = endpoint_init_hs(endpoint, err); + else { + succeeded = FALSE; + + if (err) + *err = -EINVAL; + } + + if (!succeeded) { + g_free(endpoint); + return NULL; + } + + endpoint->watch = g_dbus_add_disconnect_watch(adapter->conn, sender, + media_endpoint_exit, endpoint, + NULL); + + adapter->endpoints = g_slist_append(adapter->endpoints, endpoint); + info("Endpoint registered: sender=%s path=%s", sender, path); + + if (err) + *err = 0; + return endpoint; +} + +static struct media_endpoint *media_adapter_find_endpoint( + struct media_adapter *adapter, + const char *sender, + const char *path, + const char *uuid) +{ + GSList *l; + + for (l = adapter->endpoints; l; l = l->next) { + struct media_endpoint *endpoint = l->data; + + if (sender && g_strcmp0(endpoint->sender, sender) != 0) + continue; + + if (path && g_strcmp0(endpoint->path, path) != 0) + continue; + + if (uuid && g_strcmp0(endpoint->uuid, uuid) != 0) + continue; + + return endpoint; + } + + return NULL; +} + +static int parse_properties(DBusMessageIter *props, const char **uuid, + gboolean *delay_reporting, uint8_t *codec, + uint8_t **capabilities, int *size) +{ + gboolean has_uuid = FALSE; + gboolean has_codec = FALSE; + + while (dbus_message_iter_get_arg_type(props) == DBUS_TYPE_DICT_ENTRY) { + const char *key; + DBusMessageIter value, entry; + int var; + + dbus_message_iter_recurse(props, &entry); + dbus_message_iter_get_basic(&entry, &key); + + dbus_message_iter_next(&entry); + dbus_message_iter_recurse(&entry, &value); + + var = dbus_message_iter_get_arg_type(&value); + if (strcasecmp(key, "UUID") == 0) { + if (var != DBUS_TYPE_STRING) + return -EINVAL; + dbus_message_iter_get_basic(&value, uuid); + has_uuid = TRUE; + } else if (strcasecmp(key, "Codec") == 0) { + if (var != DBUS_TYPE_BYTE) + return -EINVAL; + dbus_message_iter_get_basic(&value, codec); + has_codec = TRUE; + } else if (strcasecmp(key, "DelayReporting") == 0) { + if (var != DBUS_TYPE_BOOLEAN) + return -EINVAL; + dbus_message_iter_get_basic(&value, delay_reporting); + } else if (strcasecmp(key, "Capabilities") == 0) { + DBusMessageIter array; + + if (var != DBUS_TYPE_ARRAY) + return -EINVAL; + + dbus_message_iter_recurse(&value, &array); + dbus_message_iter_get_fixed_array(&array, capabilities, + size); + } + + dbus_message_iter_next(props); + } + + return (has_uuid && has_codec) ? 0 : -EINVAL; +} + +static DBusMessage *register_endpoint(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_adapter *adapter = data; + DBusMessageIter args, props; + const char *sender, *path, *uuid; + gboolean delay_reporting = FALSE; + uint8_t codec; + uint8_t *capabilities; + int size = 0; + int err; + + sender = dbus_message_get_sender(msg); + + dbus_message_iter_init(msg, &args); + + dbus_message_iter_get_basic(&args, &path); + dbus_message_iter_next(&args); + + if (media_adapter_find_endpoint(adapter, sender, path, NULL) != NULL) + return btd_error_already_exists(msg); + + dbus_message_iter_recurse(&args, &props); + if (dbus_message_iter_get_arg_type(&props) != DBUS_TYPE_DICT_ENTRY) + return btd_error_invalid_args(msg); + + if (parse_properties(&props, &uuid, &delay_reporting, &codec, + &capabilities, &size) < 0) + return btd_error_invalid_args(msg); + + if (media_endpoint_create(adapter, sender, path, uuid, delay_reporting, + codec, capabilities, size, &err) == NULL) { + if (err == -EPROTONOSUPPORT) + return btd_error_not_supported(msg); + else + return btd_error_invalid_args(msg); + } + + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static DBusMessage *unregister_endpoint(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_adapter *adapter = data; + struct media_endpoint *endpoint; + const char *sender, *path; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) + return NULL; + + sender = dbus_message_get_sender(msg); + + endpoint = media_adapter_find_endpoint(adapter, sender, path, NULL); + if (endpoint == NULL) + return btd_error_does_not_exist(msg); + + media_endpoint_remove(endpoint); + + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static struct media_player *media_adapter_find_player( + struct media_adapter *adapter, + const char *sender, + const char *path) +{ + GSList *l; + + for (l = adapter->players; l; l = l->next) { + struct media_player *mp = l->data; + + if (sender && g_strcmp0(mp->sender, sender) != 0) + continue; + + if (path && g_strcmp0(mp->path, path) != 0) + continue; + + return mp; + } + + return NULL; +} + +static void release_player(struct media_player *mp) +{ + DBusMessage *msg; + + DBG("sender=%s path=%s", mp->sender, mp->path); + + msg = dbus_message_new_method_call(mp->sender, mp->path, + MEDIA_PLAYER_INTERFACE, + "Release"); + if (msg == NULL) { + error("Couldn't allocate D-Bus message"); + return; + } + + g_dbus_send_message(mp->adapter->conn, msg); +} + +static void media_player_free(gpointer data) +{ + struct media_player *mp = data; + struct media_adapter *adapter = mp->adapter; + + if (mp->player) { + adapter->players = g_slist_remove(adapter->players, mp); + release_player(mp); + } + + g_dbus_remove_watch(adapter->conn, mp->watch); + g_dbus_remove_watch(adapter->conn, mp->property_watch); + g_dbus_remove_watch(adapter->conn, mp->track_watch); + + if (mp->track) + g_hash_table_unref(mp->track); + + if (mp->settings) + g_hash_table_unref(mp->settings); + + g_timer_destroy(mp->timer); + g_free(mp->sender); + g_free(mp->path); + g_free(mp); +} + +static void media_player_destroy(struct media_player *mp) +{ + struct media_adapter *adapter = mp->adapter; + + DBG("sender=%s path=%s", mp->sender, mp->path); + + if (mp->player) { + struct avrcp_player *player = mp->player; + mp->player = NULL; + adapter->players = g_slist_remove(adapter->players, mp); + avrcp_unregister_player(player); + return; + } + + media_player_free(mp); +} + +static void media_player_remove(struct media_player *mp) +{ + info("Player unregistered: sender=%s path=%s", mp->sender, mp->path); + + media_player_destroy(mp); +} + +static const char *attrval_to_str(uint8_t attr, uint8_t value) +{ + switch (attr) { + case AVRCP_ATTRIBUTE_EQUALIZER: + switch (value) { + case AVRCP_EQUALIZER_ON: + return "on"; + case AVRCP_EQUALIZER_OFF: + return "off"; + } + + break; + case AVRCP_ATTRIBUTE_REPEAT_MODE: + switch (value) { + case AVRCP_REPEAT_MODE_OFF: + return "off"; + case AVRCP_REPEAT_MODE_SINGLE: + return "singletrack"; + case AVRCP_REPEAT_MODE_ALL: + return "alltracks"; + case AVRCP_REPEAT_MODE_GROUP: + return "group"; + } + + break; + /* Shuffle and scan have the same values */ + case AVRCP_ATTRIBUTE_SHUFFLE: + case AVRCP_ATTRIBUTE_SCAN: + switch (value) { + case AVRCP_SCAN_OFF: + return "off"; + case AVRCP_SCAN_ALL: + return "alltracks"; + case AVRCP_SCAN_GROUP: + return "group"; + } + + break; + } + + return NULL; +} + +static int attrval_to_val(uint8_t attr, const char *value) +{ + int ret; + + switch (attr) { + case AVRCP_ATTRIBUTE_EQUALIZER: + if (!strcmp(value, "off")) + ret = AVRCP_EQUALIZER_OFF; + else if (!strcmp(value, "on")) + ret = AVRCP_EQUALIZER_ON; + else + ret = -EINVAL; + + return ret; + case AVRCP_ATTRIBUTE_REPEAT_MODE: + if (!strcmp(value, "off")) + ret = AVRCP_REPEAT_MODE_OFF; + else if (!strcmp(value, "singletrack")) + ret = AVRCP_REPEAT_MODE_SINGLE; + else if (!strcmp(value, "alltracks")) + ret = AVRCP_REPEAT_MODE_ALL; + else if (!strcmp(value, "group")) + ret = AVRCP_REPEAT_MODE_GROUP; + else + ret = -EINVAL; + + return ret; + case AVRCP_ATTRIBUTE_SHUFFLE: + if (!strcmp(value, "off")) + ret = AVRCP_SHUFFLE_OFF; + else if (!strcmp(value, "alltracks")) + ret = AVRCP_SHUFFLE_ALL; + else if (!strcmp(value, "group")) + ret = AVRCP_SHUFFLE_GROUP; + else + ret = -EINVAL; + + return ret; + case AVRCP_ATTRIBUTE_SCAN: + if (!strcmp(value, "off")) + ret = AVRCP_SCAN_OFF; + else if (!strcmp(value, "alltracks")) + ret = AVRCP_SCAN_ALL; + else if (!strcmp(value, "group")) + ret = AVRCP_SCAN_GROUP; + else + ret = -EINVAL; + + return ret; + } + + return -EINVAL; +} + +static const char *attr_to_str(uint8_t attr) +{ + switch (attr) { + case AVRCP_ATTRIBUTE_EQUALIZER: + return "Equalizer"; + case AVRCP_ATTRIBUTE_REPEAT_MODE: + return "Repeat"; + case AVRCP_ATTRIBUTE_SHUFFLE: + return "Shuffle"; + case AVRCP_ATTRIBUTE_SCAN: + return "Scan"; + } + + return NULL; +} + +static int attr_to_val(const char *str) +{ + if (!strcasecmp(str, "Equalizer")) + return AVRCP_ATTRIBUTE_EQUALIZER; + else if (!strcasecmp(str, "Repeat")) + return AVRCP_ATTRIBUTE_REPEAT_MODE; + else if (!strcasecmp(str, "Shuffle")) + return AVRCP_ATTRIBUTE_SHUFFLE; + else if (!strcasecmp(str, "Scan")) + return AVRCP_ATTRIBUTE_SCAN; + + return -EINVAL; +} + +static int play_status_to_val(const char *status) +{ + if (!strcasecmp(status, "stopped")) + return AVRCP_PLAY_STATUS_STOPPED; + else if (!strcasecmp(status, "playing")) + return AVRCP_PLAY_STATUS_PLAYING; + else if (!strcasecmp(status, "paused")) + return AVRCP_PLAY_STATUS_PAUSED; + else if (!strcasecmp(status, "forward-seek")) + return AVRCP_PLAY_STATUS_FWD_SEEK; + else if (!strcasecmp(status, "reverse-seek")) + return AVRCP_PLAY_STATUS_REV_SEEK; + else if (!strcasecmp(status, "error")) + return AVRCP_PLAY_STATUS_ERROR; + + return -EINVAL; +} + +static int metadata_to_val(const char *str) +{ + if (!strcasecmp(str, "Title")) + return AVRCP_MEDIA_ATTRIBUTE_TITLE; + else if (!strcasecmp(str, "Artist")) + return AVRCP_MEDIA_ATTRIBUTE_ARTIST; + else if (!strcasecmp(str, "Album")) + return AVRCP_MEDIA_ATTRIBUTE_ALBUM; + else if (!strcasecmp(str, "Genre")) + return AVRCP_MEDIA_ATTRIBUTE_GENRE; + else if (!strcasecmp(str, "NumberOfTracks")) + return AVRCP_MEDIA_ATTRIBUTE_N_TRACKS; + else if (!strcasecmp(str, "Number")) + return AVRCP_MEDIA_ATTRIBUTE_TRACK; + else if (!strcasecmp(str, "Duration")) + return AVRCP_MEDIA_ATTRIBUTE_DURATION; + + return -EINVAL; +} + +static const char *metadata_to_str(uint32_t id) +{ + switch (id) { + case AVRCP_MEDIA_ATTRIBUTE_TITLE: + return "Title"; + case AVRCP_MEDIA_ATTRIBUTE_ARTIST: + return "Artist"; + case AVRCP_MEDIA_ATTRIBUTE_ALBUM: + return "Album"; + case AVRCP_MEDIA_ATTRIBUTE_GENRE: + return "Genre"; + case AVRCP_MEDIA_ATTRIBUTE_TRACK: + return "Track"; + case AVRCP_MEDIA_ATTRIBUTE_N_TRACKS: + return "NumberOfTracks"; + case AVRCP_MEDIA_ATTRIBUTE_DURATION: + return "Duration"; + } + + return NULL; +} + +static int get_setting(uint8_t attr, void *user_data) +{ + struct media_player *mp = user_data; + guint attr_uint = attr; + void *value; + + DBG("%s", attr_to_str(attr)); + + value = g_hash_table_lookup(mp->settings, GUINT_TO_POINTER(attr_uint)); + if (!value) + return -EINVAL; + + return GPOINTER_TO_UINT(value); +} + +static int set_setting(uint8_t attr, uint8_t val, void *user_data) +{ + struct media_player *mp = user_data; + struct media_adapter *adapter = mp->adapter; + const char *property, *value; + guint attr_uint = attr; + DBusMessage *msg; + DBusMessageIter iter, var; + + property = attr_to_str(attr); + value = attrval_to_str(attr, val); + + DBG("%s = %s", property, value); + + if (property == NULL || value == NULL) + return -EINVAL; + + if (!g_hash_table_lookup(mp->settings, GUINT_TO_POINTER(attr_uint))) + return -EINVAL; + + msg = dbus_message_new_method_call(mp->sender, mp->path, + MEDIA_PLAYER_INTERFACE, + "SetProperty"); + if (msg == NULL) { + error("Couldn't allocate D-Bus message"); + return -ENOMEM; + } + + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &property); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, + &var); + dbus_message_iter_append_basic(&var, DBUS_TYPE_STRING, &value); + dbus_message_iter_close_container(&iter, &var); + + g_dbus_send_message(adapter->conn, msg); + + return 0; +} + +static GList *list_metadata(void *user_data) +{ + struct media_player *mp = user_data; + + DBG(""); + + if (mp->track == NULL) + return NULL; + + return g_hash_table_get_keys(mp->track); +} + +static uint64_t get_uid(void *user_data) +{ + struct media_player *mp = user_data; + + DBG("%p", mp->track); + + if (mp->track == NULL) + return UINT64_MAX; + + return 0; +} + +static void *get_metadata(uint32_t id, void *user_data) +{ + struct media_player *mp = user_data; + struct metadata_value *value; + + DBG("%s", metadata_to_str(id)); + + if (mp->track == NULL) + return NULL; + + value = g_hash_table_lookup(mp->track, GUINT_TO_POINTER(id)); + if (!value) + return NULL; + + switch (value->type) { + case DBUS_TYPE_STRING: + return value->value.str; + case DBUS_TYPE_UINT32: + return GUINT_TO_POINTER(value->value.num); + } + + return NULL; +} + +static uint8_t get_status(void *user_data) +{ + struct media_player *mp = user_data; + + return mp->status; +} + +static uint32_t get_position(void *user_data) +{ + struct media_player *mp = user_data; + double timedelta; + uint32_t sec, msec; + + if (mp->status != AVRCP_PLAY_STATUS_PLAYING) + return mp->position; + + timedelta = g_timer_elapsed(mp->timer, NULL); + + sec = (uint32_t) timedelta; + msec = (uint32_t) ((timedelta - sec) * 1000); + + return mp->position + sec * 1000 + msec; +} + +static void set_volume(uint8_t volume, struct audio_device *dev, void *user_data) +{ + struct media_player *mp = user_data; + GSList *l; + + if (mp->volume == volume) + return; + + mp->volume = volume; + + for (l = mp->adapter->endpoints; l; l = l->next) { + struct media_endpoint *endpoint = l->data; + struct media_transport *transport; + + /* Volume is A2DP only */ + if (endpoint->sep == NULL) + continue; + + transport = find_device_transport(endpoint, dev); + if (transport == NULL) + continue; + + media_transport_update_volume(transport, volume); + } +} + +static struct avrcp_player_cb player_cb = { + .get_setting = get_setting, + .set_setting = set_setting, + .list_metadata = list_metadata, + .get_uid = get_uid, + .get_metadata = get_metadata, + .get_position = get_position, + .get_status = get_status, + .set_volume = set_volume +}; + +static void media_player_exit(DBusConnection *connection, void *user_data) +{ + struct media_player *mp = user_data; + + mp->watch = 0; + media_player_remove(mp); +} + +static gboolean set_status(struct media_player *mp, DBusMessageIter *iter) +{ + const char *value; + int val; + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRING) + return FALSE; + + dbus_message_iter_get_basic(iter, &value); + DBG("Status=%s", value); + + val = play_status_to_val(value); + if (val < 0) { + error("Invalid status"); + return FALSE; + } + + if (mp->status == val) + return TRUE; + + mp->position = get_position(mp); + g_timer_start(mp->timer); + + mp->status = val; + + avrcp_player_event(mp->player, AVRCP_EVENT_STATUS_CHANGED, &val); + + return TRUE; +} + +static gboolean set_position(struct media_player *mp, DBusMessageIter *iter) +{ + uint32_t value; + struct metadata_value *duration; + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_UINT32) + return FALSE; + + dbus_message_iter_get_basic(iter, &value); + DBG("Position=%u", value); + + mp->position = value; + g_timer_start(mp->timer); + + if (!mp->position) { + avrcp_player_event(mp->player, + AVRCP_EVENT_TRACK_REACHED_START, NULL); + return TRUE; + } + + duration = g_hash_table_lookup(mp->track, GUINT_TO_POINTER( + AVRCP_MEDIA_ATTRIBUTE_DURATION)); + + /* + * If position is the maximum value allowed or greater than track's + * duration, we send a track-reached-end event. + */ + if (mp->position == UINT32_MAX || + (duration && mp->position >= duration->value.num)) + avrcp_player_event(mp->player, AVRCP_EVENT_TRACK_REACHED_END, + NULL); + + return TRUE; +} + +static gboolean set_property(struct media_player *mp, const char *key, + DBusMessageIter *entry) +{ + DBusMessageIter var; + const char *value; + int attr, val; + + if (dbus_message_iter_get_arg_type(entry) != DBUS_TYPE_VARIANT) + return FALSE; + + dbus_message_iter_recurse(entry, &var); + + if (strcasecmp(key, "Status") == 0) + return set_status(mp, &var); + + if (strcasecmp(key, "Position") == 0) + return set_position(mp, &var); + + attr = attr_to_val(key); + if (attr < 0) + return FALSE; + + if (dbus_message_iter_get_arg_type(&var) != DBUS_TYPE_STRING) + return FALSE; + + dbus_message_iter_get_basic(&var, &value); + + val = attrval_to_val(attr, value); + if (val < 0) + return FALSE; + + DBG("%s=%s", key, value); + + g_hash_table_replace(mp->settings, GUINT_TO_POINTER(attr), + GUINT_TO_POINTER(val)); + + return TRUE; +} + +static gboolean property_changed(DBusConnection *connection, DBusMessage *msg, + void *user_data) +{ + struct media_player *mp = user_data; + DBusMessageIter iter; + const char *property; + + DBG("sender=%s path=%s", mp->sender, mp->path); + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) { + error("Unexpected signature in %s.%s signal", + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + return TRUE; + } + + dbus_message_iter_get_basic(&iter, &property); + + dbus_message_iter_next(&iter); + + set_property(mp, property, &iter); + + return TRUE; +} + +static void metadata_value_free(gpointer data) +{ + struct metadata_value *value = data; + + switch (value->type) { + case DBUS_TYPE_STRING: + g_free(value->value.str); + break; + } + + g_free(value); +} + +static gboolean parse_player_metadata(struct media_player *mp, + DBusMessageIter *iter) +{ + DBusMessageIter dict; + DBusMessageIter var; + GHashTable *track; + int ctype; + gboolean title = FALSE; + uint64_t uid; + + ctype = dbus_message_iter_get_arg_type(iter); + if (ctype != DBUS_TYPE_ARRAY) + return FALSE; + + dbus_message_iter_recurse(iter, &dict); + + track = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, + metadata_value_free); + + while ((ctype = dbus_message_iter_get_arg_type(&dict)) != + DBUS_TYPE_INVALID) { + DBusMessageIter entry; + const char *key; + struct metadata_value *value; + int id; + + if (ctype != DBUS_TYPE_DICT_ENTRY) + goto parse_error; + + dbus_message_iter_recurse(&dict, &entry); + if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_STRING) + goto parse_error; + + dbus_message_iter_get_basic(&entry, &key); + dbus_message_iter_next(&entry); + + id = metadata_to_val(key); + if (id < 0) + goto parse_error; + + if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_VARIANT) + goto parse_error; + + dbus_message_iter_recurse(&entry, &var); + + value = g_new0(struct metadata_value, 1); + value->type = dbus_message_iter_get_arg_type(&var); + + switch (id) { + case AVRCP_MEDIA_ATTRIBUTE_TITLE: + title = TRUE; + case AVRCP_MEDIA_ATTRIBUTE_ARTIST: + case AVRCP_MEDIA_ATTRIBUTE_ALBUM: + case AVRCP_MEDIA_ATTRIBUTE_GENRE: + if (value->type != DBUS_TYPE_STRING) { + g_free(value); + goto parse_error; + } + + dbus_message_iter_get_basic(&var, &value->value.str); + break; + case AVRCP_MEDIA_ATTRIBUTE_TRACK: + case AVRCP_MEDIA_ATTRIBUTE_N_TRACKS: + case AVRCP_MEDIA_ATTRIBUTE_DURATION: + if (value->type != DBUS_TYPE_UINT32) { + g_free(value); + goto parse_error; + } + + dbus_message_iter_get_basic(&var, &value->value.num); + break; + default: + goto parse_error; + } + + switch (value->type) { + case DBUS_TYPE_STRING: + value->value.str = g_strdup(value->value.str); + DBG("%s=%s", key, value->value.str); + break; + default: + DBG("%s=%u", key, value->value.num); + } + + g_hash_table_replace(track, GUINT_TO_POINTER(id), value); + dbus_message_iter_next(&dict); + } + + if (g_hash_table_size(track) == 0) { + g_hash_table_unref(track); + track = NULL; + } else if (title == FALSE) { + struct metadata_value *value = g_new(struct metadata_value, 1); + uint32_t id = AVRCP_MEDIA_ATTRIBUTE_TITLE; + + value->type = DBUS_TYPE_STRING; + value->value.str = g_strdup(""); + g_hash_table_insert(track, GUINT_TO_POINTER(id), value); + } + + if (mp->track != NULL) + g_hash_table_unref(mp->track); + + mp->track = track; + mp->position = 0; + g_timer_start(mp->timer); + uid = get_uid(mp); + + avrcp_player_event(mp->player, AVRCP_EVENT_TRACK_CHANGED, &uid); + avrcp_player_event(mp->player, AVRCP_EVENT_TRACK_REACHED_START, + NULL); + + return TRUE; + +parse_error: + if (track) + g_hash_table_unref(track); + + return FALSE; +} + +static gboolean track_changed(DBusConnection *connection, DBusMessage *msg, + void *user_data) +{ + struct media_player *mp = user_data; + DBusMessageIter iter; + + DBG("sender=%s path=%s", mp->sender, mp->path); + + dbus_message_iter_init(msg, &iter); + + if (parse_player_metadata(mp, &iter) == FALSE) { + error("Unexpected signature in %s.%s signal", + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + } + + return TRUE; +} + +static struct media_player *media_player_create(struct media_adapter *adapter, + const char *sender, + const char *path, + int *err) +{ + struct media_player *mp; + + mp = g_new0(struct media_player, 1); + mp->adapter = adapter; + mp->sender = g_strdup(sender); + mp->path = g_strdup(path); + mp->timer = g_timer_new(); + + mp->watch = g_dbus_add_disconnect_watch(adapter->conn, sender, + media_player_exit, mp, + NULL); + mp->property_watch = g_dbus_add_signal_watch(adapter->conn, sender, + path, MEDIA_PLAYER_INTERFACE, + "PropertyChanged", + property_changed, + mp, NULL); + mp->track_watch = g_dbus_add_signal_watch(adapter->conn, sender, + path, MEDIA_PLAYER_INTERFACE, + "TrackChanged", + track_changed, + mp, NULL); + mp->player = avrcp_register_player(&adapter->src, &player_cb, mp, + media_player_free); + if (!mp->player) { + if (err) + *err = -EPROTONOSUPPORT; + media_player_destroy(mp); + return NULL; + } + + mp->settings = g_hash_table_new(g_direct_hash, g_direct_equal); + + adapter->players = g_slist_append(adapter->players, mp); + + info("Player registered: sender=%s path=%s", sender, path); + + if (err) + *err = 0; + + return mp; +} + +static gboolean parse_player_properties(struct media_player *mp, + DBusMessageIter *iter) +{ + DBusMessageIter dict; + int ctype; + + ctype = dbus_message_iter_get_arg_type(iter); + if (ctype != DBUS_TYPE_ARRAY) + return FALSE; + + dbus_message_iter_recurse(iter, &dict); + + while ((ctype = dbus_message_iter_get_arg_type(&dict)) != + DBUS_TYPE_INVALID) { + DBusMessageIter entry; + const char *key; + + if (ctype != DBUS_TYPE_DICT_ENTRY) + return FALSE; + + dbus_message_iter_recurse(&dict, &entry); + if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_STRING) + return FALSE; + + dbus_message_iter_get_basic(&entry, &key); + dbus_message_iter_next(&entry); + + if (set_property(mp, key, &entry) == FALSE) + return FALSE; + + dbus_message_iter_next(&dict); + } + + return TRUE; +} + +static DBusMessage *register_player(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_adapter *adapter = data; + struct media_player *mp; + DBusMessageIter args; + const char *sender, *path; + int err; + + sender = dbus_message_get_sender(msg); + + dbus_message_iter_init(msg, &args); + + dbus_message_iter_get_basic(&args, &path); + dbus_message_iter_next(&args); + + if (media_adapter_find_player(adapter, sender, path) != NULL) + return btd_error_already_exists(msg); + + mp = media_player_create(adapter, sender, path, &err); + if (mp == NULL) { + if (err == -EPROTONOSUPPORT) + return btd_error_not_supported(msg); + else + return btd_error_invalid_args(msg); + } + + if (parse_player_properties(mp, &args) == FALSE) { + media_player_destroy(mp); + return btd_error_invalid_args(msg); + } + + dbus_message_iter_next(&args); + + if (parse_player_metadata(mp, &args) == FALSE) { + media_player_destroy(mp); + return btd_error_invalid_args(msg); + } + + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static DBusMessage *unregister_player(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_adapter *adapter = data; + struct media_player *player; + const char *sender, *path; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) + return NULL; + + sender = dbus_message_get_sender(msg); + + player = media_adapter_find_player(adapter, sender, path); + if (player == NULL) + return btd_error_does_not_exist(msg); + + media_player_remove(player); + + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static const GDBusMethodTable media_methods[] = { + { GDBUS_METHOD("RegisterEndpoint", + GDBUS_ARGS({ "endpoint", "o" }, { "properties", "a{sv}" }), + NULL, register_endpoint) }, + { GDBUS_METHOD("UnregisterEndpoint", + GDBUS_ARGS({ "endpoint", "o" }), NULL, unregister_endpoint) }, + { GDBUS_METHOD("RegisterPlayer", + GDBUS_ARGS({ "player", "o" }, { "properties", "a{sv}" }, + { "metadata", "a{sv}" }), + NULL, register_player) }, + { GDBUS_METHOD("UnregisterPlayer", + GDBUS_ARGS({ "player", "o" }), NULL, unregister_player) }, + { }, +}; + +static void path_free(void *data) +{ + struct media_adapter *adapter = data; + + while (adapter->endpoints) + release_endpoint(adapter->endpoints->data); + + dbus_connection_unref(adapter->conn); + + adapters = g_slist_remove(adapters, adapter); + + g_free(adapter->path); + g_free(adapter); +} + +int media_register(DBusConnection *conn, const char *path, const bdaddr_t *src) +{ + struct media_adapter *adapter; + + adapter = g_new0(struct media_adapter, 1); + adapter->conn = dbus_connection_ref(conn); + bacpy(&adapter->src, src); + adapter->path = g_strdup(path); + + if (!g_dbus_register_interface(conn, path, MEDIA_INTERFACE, + media_methods, NULL, NULL, + adapter, path_free)) { + error("D-Bus failed to register %s path", path); + path_free(adapter); + return -1; + } + + adapters = g_slist_append(adapters, adapter); + + return 0; +} + +void media_unregister(const char *path) +{ + GSList *l; + + for (l = adapters; l; l = l->next) { + struct media_adapter *adapter = l->data; + + if (g_strcmp0(path, adapter->path) == 0) { + g_dbus_unregister_interface(adapter->conn, path, + MEDIA_INTERFACE); + return; + } + } +} + +struct a2dp_sep *media_endpoint_get_sep(struct media_endpoint *endpoint) +{ + return endpoint->sep; +} + +const char *media_endpoint_get_uuid(struct media_endpoint *endpoint) +{ + return endpoint->uuid; +} + +uint8_t media_endpoint_get_codec(struct media_endpoint *endpoint) +{ + return endpoint->codec; +} diff --git a/audio/media.h b/audio/media.h new file mode 100644 index 0000000..ee9a51e --- /dev/null +++ b/audio/media.h @@ -0,0 +1,37 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2007 Nokia Corporation + * Copyright (C) 2004-2009 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +struct media_endpoint; + +typedef void (*media_endpoint_cb_t) (struct media_endpoint *endpoint, + void *ret, int size, void *user_data); + +int media_register(DBusConnection *conn, const char *path, const bdaddr_t *src); +void media_unregister(const char *path); + +struct a2dp_sep *media_endpoint_get_sep(struct media_endpoint *endpoint); +const char *media_endpoint_get_uuid(struct media_endpoint *endpoint); +uint8_t media_endpoint_get_codec(struct media_endpoint *endpoint); +struct media_transport *media_endpoint_get_transport( + struct media_endpoint *endpoint); diff --git a/audio/pcm_bluetooth.c b/audio/pcm_bluetooth.c new file mode 100644 index 0000000..b9da805 --- /dev/null +++ b/audio/pcm_bluetooth.c @@ -0,0 +1,1785 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "ipc.h" +#include "sbc.h" +#include "rtp.h" + +/* #define ENABLE_DEBUG */ + +#define UINT_SECS_MAX (UINT_MAX / 1000000 - 1) + +#define MIN_PERIOD_TIME 1 + +#define BUFFER_SIZE 2048 + +#ifdef ENABLE_DEBUG +#define DBG(fmt, arg...) printf("DEBUG: %s: " fmt "\n" , __FUNCTION__ , ## arg) +#else +#define DBG(fmt, arg...) +#endif + +#ifndef SOL_SCO +#define SOL_SCO 17 +#endif + +#ifndef SCO_TXBUFS +#define SCO_TXBUFS 0x03 +#endif + +#ifndef SCO_RXBUFS +#define SCO_RXBUFS 0x04 +#endif + +#ifndef MIN +# define MIN(x, y) ((x) < (y) ? (x) : (y)) +#endif + +#ifndef MAX +# define MAX(x, y) ((x) > (y) ? (x) : (y)) +#endif + +#define MAX_BITPOOL 64 +#define MIN_BITPOOL 2 + +/* adapted from glibc sys/time.h timersub() macro */ +#define priv_timespecsub(a, b, result) \ + do { \ + (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \ + (result)->tv_nsec = (a)->tv_nsec - (b)->tv_nsec; \ + if ((result)->tv_nsec < 0) { \ + --(result)->tv_sec; \ + (result)->tv_nsec += 1000000000; \ + } \ + } while (0) + +struct bluetooth_a2dp { + sbc_capabilities_t sbc_capabilities; + sbc_t sbc; /* Codec data */ + int sbc_initialized; /* Keep track if the encoder is initialized */ + unsigned int codesize; /* SBC codesize */ + int samples; /* Number of encoded samples */ + uint8_t buffer[BUFFER_SIZE]; /* Codec transfer buffer */ + unsigned int count; /* Codec transfer buffer counter */ + + int nsamples; /* Cumulative number of codec samples */ + uint16_t seq_num; /* Cumulative packet sequence */ + int frame_count; /* Current frames in buffer*/ +}; + +struct bluetooth_alsa_config { + char device[18]; /* Address of the remote Device */ + int has_device; + uint8_t transport; /* Requested transport */ + int has_transport; + uint16_t rate; + int has_rate; + uint8_t channel_mode; /* A2DP only */ + int has_channel_mode; + uint8_t allocation_method; /* A2DP only */ + int has_allocation_method; + uint8_t subbands; /* A2DP only */ + int has_subbands; + uint8_t block_length; /* A2DP only */ + int has_block_length; + uint8_t bitpool; /* A2DP only */ + int has_bitpool; + int autoconnect; +}; + +struct bluetooth_data { + snd_pcm_ioplug_t io; + struct bluetooth_alsa_config alsa_config; /* ALSA resource file parameters */ + volatile snd_pcm_sframes_t hw_ptr; + int transport; /* chosen transport SCO or AD2P */ + unsigned int link_mtu; /* MTU for selected transport channel */ + volatile struct pollfd stream; /* Audio stream filedescriptor */ + struct pollfd server; /* Audio daemon filedescriptor */ + uint8_t buffer[BUFFER_SIZE]; /* Encoded transfer buffer */ + unsigned int count; /* Transfer buffer counter */ + struct bluetooth_a2dp a2dp; /* A2DP data */ + + pthread_t hw_thread; /* Makes virtual hw pointer move */ + int pipefd[2]; /* Inter thread communication */ + int stopped; + sig_atomic_t reset; /* Request XRUN handling */ +}; + +static int audioservice_send(int sk, const bt_audio_msg_header_t *msg); +static int audioservice_expect(int sk, bt_audio_msg_header_t *outmsg, + int expected_type); + +static int bluetooth_start(snd_pcm_ioplug_t *io) +{ + DBG("bluetooth_start %p", io); + + return 0; +} + +static int bluetooth_stop(snd_pcm_ioplug_t *io) +{ + DBG("bluetooth_stop %p", io); + + return 0; +} + +static void *playback_hw_thread(void *param) +{ + struct bluetooth_data *data = param; + unsigned int prev_periods; + double period_time; + struct timespec start; + struct pollfd fds[2]; + int poll_timeout; + + data->server.events = POLLIN; + /* note: only errors for data->stream.events */ + + fds[0] = data->server; + fds[1] = data->stream; + + prev_periods = 0; + period_time = 1000000.0 * data->io.period_size / data->io.rate; + if (period_time > (int) (MIN_PERIOD_TIME * 1000)) + poll_timeout = (int) (period_time / 1000.0f); + else + poll_timeout = MIN_PERIOD_TIME; + + clock_gettime(CLOCK_MONOTONIC, &start); + + while (1) { + unsigned int dtime, periods; + struct timespec cur, delta; + int ret; + + if (data->stopped) + goto iter_sleep; + + if (data->reset) { + DBG("Handle XRUN in hw-thread."); + data->reset = 0; + clock_gettime(CLOCK_MONOTONIC, &start); + prev_periods = 0; + } + + clock_gettime(CLOCK_MONOTONIC, &cur); + + priv_timespecsub(&cur, &start, &delta); + + dtime = delta.tv_sec * 1000000 + delta.tv_nsec / 1000; + periods = 1.0 * dtime / period_time; + + if (periods > prev_periods) { + char c = 'w'; + int frags = periods - prev_periods, n; + + data->hw_ptr += frags * data->io.period_size; + data->hw_ptr %= data->io.buffer_size; + + for (n = 0; n < frags; n++) { + /* Notify user that hardware pointer + * has moved * */ + if (write(data->pipefd[1], &c, 1) < 0) + pthread_testcancel(); + } + + /* Reset point of reference to avoid too big values + * that wont fit an unsigned int */ + if ((unsigned int) delta.tv_sec < UINT_SECS_MAX) + prev_periods = periods; + else { + prev_periods = 0; + clock_gettime(CLOCK_MONOTONIC, &start); + } + } + +iter_sleep: + /* sleep up to one period interval */ + ret = poll(fds, 2, poll_timeout); + + if (ret < 0) { + if (errno != EINTR) { + SNDERR("poll error: %s (%d)", strerror(errno), + errno); + break; + } + } else if (ret > 0) { + ret = (fds[0].revents) ? 0 : 1; + SNDERR("poll fd %d revents %d", ret, fds[ret].revents); + if (fds[ret].revents & (POLLERR | POLLHUP | POLLNVAL)) + break; + } + + /* Offer opportunity to be canceled by main thread */ + pthread_testcancel(); + } + + data->hw_thread = 0; + pthread_exit(NULL); +} + +static int bluetooth_playback_start(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + int err; + + DBG("%p", io); + + data->stopped = 0; + + if (data->hw_thread) + return 0; + + err = pthread_create(&data->hw_thread, 0, playback_hw_thread, data); + + return -err; +} + +static int bluetooth_playback_stop(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + + DBG("%p", io); + + data->stopped = 1; + + return 0; +} + +static snd_pcm_sframes_t bluetooth_pointer(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + + return data->hw_ptr; +} + +static void bluetooth_exit(struct bluetooth_data *data) +{ + struct bluetooth_a2dp *a2dp = &data->a2dp; + + if (data->server.fd >= 0) + bt_audio_service_close(data->server.fd); + + if (data->stream.fd >= 0) + close(data->stream.fd); + + if (data->hw_thread) { + pthread_cancel(data->hw_thread); + pthread_join(data->hw_thread, 0); + } + + if (a2dp->sbc_initialized) + sbc_finish(&a2dp->sbc); + + if (data->pipefd[0] > 0) + close(data->pipefd[0]); + + if (data->pipefd[1] > 0) + close(data->pipefd[1]); + + free(data); +} + +static int bluetooth_close(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + + DBG("%p", io); + + bluetooth_exit(data); + + return 0; +} + +static int bluetooth_prepare(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + char c = 'w'; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_start_stream_req *req = (void *) buf; + struct bt_start_stream_rsp *rsp = (void *) buf; + struct bt_new_stream_ind *ind = (void *) buf; + uint32_t period_count = io->buffer_size / io->period_size; + int opt_name, err; + struct timeval t = { 0, period_count }; + + DBG("Preparing with io->period_size=%lu io->buffer_size=%lu", + io->period_size, io->buffer_size); + + data->reset = 0; + + /* As we're gonna receive messages on the server socket, we have to stop the + hw thread that is polling on it, if any */ + if (data->hw_thread) { + pthread_cancel(data->hw_thread); + pthread_join(data->hw_thread, 0); + data->hw_thread = 0; + } + + if (io->stream == SND_PCM_STREAM_PLAYBACK) + /* If not null for playback, xmms doesn't display time + * correctly */ + data->hw_ptr = 0; + else + /* ALSA library is really picky on the fact hw_ptr is not null. + * If it is, capture won't start */ + data->hw_ptr = io->period_size; + + /* send start */ + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + req->h.type = BT_REQUEST; + req->h.name = BT_START_STREAM; + req->h.length = sizeof(*req); + + err = audioservice_send(data->server.fd, &req->h); + if (err < 0) + return err; + + rsp->h.length = sizeof(*rsp); + err = audioservice_expect(data->server.fd, &rsp->h, + BT_START_STREAM); + if (err < 0) + return err; + + ind->h.length = sizeof(*ind); + err = audioservice_expect(data->server.fd, &ind->h, + BT_NEW_STREAM); + if (err < 0) + return err; + + if (data->stream.fd >= 0) + close(data->stream.fd); + + data->stream.fd = bt_audio_service_get_data_fd(data->server.fd); + if (data->stream.fd < 0) { + return -errno; + } + + if (data->transport == BT_CAPABILITIES_TRANSPORT_A2DP) { + opt_name = (io->stream == SND_PCM_STREAM_PLAYBACK) ? + SO_SNDTIMEO : SO_RCVTIMEO; + + if (setsockopt(data->stream.fd, SOL_SOCKET, opt_name, &t, + sizeof(t)) < 0) + return -errno; + } else { + opt_name = (io->stream == SND_PCM_STREAM_PLAYBACK) ? + SCO_TXBUFS : SCO_RXBUFS; + + if (setsockopt(data->stream.fd, SOL_SCO, opt_name, &period_count, + sizeof(period_count)) == 0) + return 0; + + opt_name = (io->stream == SND_PCM_STREAM_PLAYBACK) ? + SO_SNDBUF : SO_RCVBUF; + + if (setsockopt(data->stream.fd, SOL_SCO, opt_name, &period_count, + sizeof(period_count)) == 0) + return 0; + + /* FIXME : handle error codes */ + } + + /* wake up any client polling at us */ + if (write(data->pipefd[1], &c, 1) < 0) { + err = -errno; + return err; + } + + return 0; +} + +static int bluetooth_hsp_hw_params(snd_pcm_ioplug_t *io, + snd_pcm_hw_params_t *params) +{ + struct bluetooth_data *data = io->private_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_open_req *open_req = (void *) buf; + struct bt_open_rsp *open_rsp = (void *) buf; + struct bt_set_configuration_req *req = (void *) buf; + struct bt_set_configuration_rsp *rsp = (void *) buf; + int err; + + DBG("Preparing with io->period_size=%lu io->buffer_size=%lu", + io->period_size, io->buffer_size); + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + open_req->h.type = BT_REQUEST; + open_req->h.name = BT_OPEN; + open_req->h.length = sizeof(*open_req); + + strncpy(open_req->destination, data->alsa_config.device, 18); + open_req->seid = BT_A2DP_SEID_RANGE + 1; + open_req->lock = (io->stream == SND_PCM_STREAM_PLAYBACK ? + BT_WRITE_LOCK : BT_READ_LOCK); + + err = audioservice_send(data->server.fd, &open_req->h); + if (err < 0) + return err; + + open_rsp->h.length = sizeof(*open_rsp); + err = audioservice_expect(data->server.fd, &open_rsp->h, + BT_OPEN); + if (err < 0) + return err; + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + req->h.type = BT_REQUEST; + req->h.name = BT_SET_CONFIGURATION; + req->h.length = sizeof(*req); + + req->codec.transport = BT_CAPABILITIES_TRANSPORT_SCO; + req->codec.seid = BT_A2DP_SEID_RANGE + 1; + req->codec.length = sizeof(pcm_capabilities_t); + + req->h.length += req->codec.length - sizeof(req->codec); + err = audioservice_send(data->server.fd, &req->h); + if (err < 0) + return err; + + rsp->h.length = sizeof(*rsp); + err = audioservice_expect(data->server.fd, &rsp->h, + BT_SET_CONFIGURATION); + if (err < 0) + return err; + + data->transport = BT_CAPABILITIES_TRANSPORT_SCO; + data->link_mtu = rsp->link_mtu; + + return 0; +} + +static uint8_t default_bitpool(uint8_t freq, uint8_t mode) +{ + switch (freq) { + case BT_SBC_SAMPLING_FREQ_16000: + case BT_SBC_SAMPLING_FREQ_32000: + return 53; + case BT_SBC_SAMPLING_FREQ_44100: + switch (mode) { + case BT_A2DP_CHANNEL_MODE_MONO: + case BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL: + return 31; + case BT_A2DP_CHANNEL_MODE_STEREO: + case BT_A2DP_CHANNEL_MODE_JOINT_STEREO: + return 53; + default: + DBG("Invalid channel mode %u", mode); + return 53; + } + case BT_SBC_SAMPLING_FREQ_48000: + switch (mode) { + case BT_A2DP_CHANNEL_MODE_MONO: + case BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL: + return 29; + case BT_A2DP_CHANNEL_MODE_STEREO: + case BT_A2DP_CHANNEL_MODE_JOINT_STEREO: + return 51; + default: + DBG("Invalid channel mode %u", mode); + return 51; + } + default: + DBG("Invalid sampling freq %u", freq); + return 53; + } +} + +static int bluetooth_a2dp_init(struct bluetooth_data *data, + snd_pcm_hw_params_t *params) +{ + struct bluetooth_alsa_config *cfg = &data->alsa_config; + sbc_capabilities_t *cap = &data->a2dp.sbc_capabilities; + unsigned int max_bitpool, min_bitpool, rate, channels; + int dir; + + snd_pcm_hw_params_get_rate(params, &rate, &dir); + snd_pcm_hw_params_get_channels(params, &channels); + + switch (rate) { + case 48000: + cap->frequency = BT_SBC_SAMPLING_FREQ_48000; + break; + case 44100: + cap->frequency = BT_SBC_SAMPLING_FREQ_44100; + break; + case 32000: + cap->frequency = BT_SBC_SAMPLING_FREQ_32000; + break; + case 16000: + cap->frequency = BT_SBC_SAMPLING_FREQ_16000; + break; + default: + DBG("Rate %d not supported", rate); + return -1; + } + + if (cfg->has_channel_mode) + cap->channel_mode = cfg->channel_mode; + else if (channels == 2) { + if (cap->channel_mode & BT_A2DP_CHANNEL_MODE_JOINT_STEREO) + cap->channel_mode = BT_A2DP_CHANNEL_MODE_JOINT_STEREO; + else if (cap->channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) + cap->channel_mode = BT_A2DP_CHANNEL_MODE_STEREO; + else if (cap->channel_mode & BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) + cap->channel_mode = BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL; + } else { + if (cap->channel_mode & BT_A2DP_CHANNEL_MODE_MONO) + cap->channel_mode = BT_A2DP_CHANNEL_MODE_MONO; + } + + if (!cap->channel_mode) { + DBG("No supported channel modes"); + return -1; + } + + if (cfg->has_block_length) + cap->block_length = cfg->block_length; + else if (cap->block_length & BT_A2DP_BLOCK_LENGTH_16) + cap->block_length = BT_A2DP_BLOCK_LENGTH_16; + else if (cap->block_length & BT_A2DP_BLOCK_LENGTH_12) + cap->block_length = BT_A2DP_BLOCK_LENGTH_12; + else if (cap->block_length & BT_A2DP_BLOCK_LENGTH_8) + cap->block_length = BT_A2DP_BLOCK_LENGTH_8; + else if (cap->block_length & BT_A2DP_BLOCK_LENGTH_4) + cap->block_length = BT_A2DP_BLOCK_LENGTH_4; + else { + DBG("No supported block lengths"); + return -1; + } + + if (cfg->has_subbands) + cap->subbands = cfg->subbands; + if (cap->subbands & BT_A2DP_SUBBANDS_8) + cap->subbands = BT_A2DP_SUBBANDS_8; + else if (cap->subbands & BT_A2DP_SUBBANDS_4) + cap->subbands = BT_A2DP_SUBBANDS_4; + else { + DBG("No supported subbands"); + return -1; + } + + if (cfg->has_allocation_method) + cap->allocation_method = cfg->allocation_method; + if (cap->allocation_method & BT_A2DP_ALLOCATION_LOUDNESS) + cap->allocation_method = BT_A2DP_ALLOCATION_LOUDNESS; + else if (cap->allocation_method & BT_A2DP_ALLOCATION_SNR) + cap->allocation_method = BT_A2DP_ALLOCATION_SNR; + + if (cfg->has_bitpool) + min_bitpool = max_bitpool = cfg->bitpool; + else { + min_bitpool = MAX(MIN_BITPOOL, cap->min_bitpool); + max_bitpool = MIN(default_bitpool(cap->frequency, + cap->channel_mode), + cap->max_bitpool); + } + + cap->min_bitpool = min_bitpool; + cap->max_bitpool = max_bitpool; + + return 0; +} + +static void bluetooth_a2dp_setup(struct bluetooth_a2dp *a2dp) +{ + sbc_capabilities_t active_capabilities = a2dp->sbc_capabilities; + + if (a2dp->sbc_initialized) + sbc_reinit(&a2dp->sbc, 0); + else + sbc_init(&a2dp->sbc, 0); + a2dp->sbc_initialized = 1; + + if (active_capabilities.frequency & BT_SBC_SAMPLING_FREQ_16000) + a2dp->sbc.frequency = SBC_FREQ_16000; + + if (active_capabilities.frequency & BT_SBC_SAMPLING_FREQ_32000) + a2dp->sbc.frequency = SBC_FREQ_32000; + + if (active_capabilities.frequency & BT_SBC_SAMPLING_FREQ_44100) + a2dp->sbc.frequency = SBC_FREQ_44100; + + if (active_capabilities.frequency & BT_SBC_SAMPLING_FREQ_48000) + a2dp->sbc.frequency = SBC_FREQ_48000; + + if (active_capabilities.channel_mode & BT_A2DP_CHANNEL_MODE_MONO) + a2dp->sbc.mode = SBC_MODE_MONO; + + if (active_capabilities.channel_mode & BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL) + a2dp->sbc.mode = SBC_MODE_DUAL_CHANNEL; + + if (active_capabilities.channel_mode & BT_A2DP_CHANNEL_MODE_STEREO) + a2dp->sbc.mode = SBC_MODE_STEREO; + + if (active_capabilities.channel_mode & BT_A2DP_CHANNEL_MODE_JOINT_STEREO) + a2dp->sbc.mode = SBC_MODE_JOINT_STEREO; + + a2dp->sbc.allocation = active_capabilities.allocation_method + == BT_A2DP_ALLOCATION_SNR ? SBC_AM_SNR + : SBC_AM_LOUDNESS; + + switch (active_capabilities.subbands) { + case BT_A2DP_SUBBANDS_4: + a2dp->sbc.subbands = SBC_SB_4; + break; + case BT_A2DP_SUBBANDS_8: + a2dp->sbc.subbands = SBC_SB_8; + break; + } + + switch (active_capabilities.block_length) { + case BT_A2DP_BLOCK_LENGTH_4: + a2dp->sbc.blocks = SBC_BLK_4; + break; + case BT_A2DP_BLOCK_LENGTH_8: + a2dp->sbc.blocks = SBC_BLK_8; + break; + case BT_A2DP_BLOCK_LENGTH_12: + a2dp->sbc.blocks = SBC_BLK_12; + break; + case BT_A2DP_BLOCK_LENGTH_16: + a2dp->sbc.blocks = SBC_BLK_16; + break; + } + + a2dp->sbc.bitpool = active_capabilities.max_bitpool; + a2dp->codesize = sbc_get_codesize(&a2dp->sbc); + a2dp->count = sizeof(struct rtp_header) + sizeof(struct rtp_payload); +} + +static int bluetooth_a2dp_hw_params(snd_pcm_ioplug_t *io, + snd_pcm_hw_params_t *params) +{ + struct bluetooth_data *data = io->private_data; + struct bluetooth_a2dp *a2dp = &data->a2dp; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_open_req *open_req = (void *) buf; + struct bt_open_rsp *open_rsp = (void *) buf; + struct bt_set_configuration_req *req = (void *) buf; + struct bt_set_configuration_rsp *rsp = (void *) buf; + int err; + + DBG("Preparing with io->period_size=%lu io->buffer_size=%lu", + io->period_size, io->buffer_size); + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + open_req->h.type = BT_REQUEST; + open_req->h.name = BT_OPEN; + open_req->h.length = sizeof(*open_req); + + strncpy(open_req->destination, data->alsa_config.device, 18); + open_req->seid = a2dp->sbc_capabilities.capability.seid; + open_req->lock = (io->stream == SND_PCM_STREAM_PLAYBACK ? + BT_WRITE_LOCK : BT_READ_LOCK); + + err = audioservice_send(data->server.fd, &open_req->h); + if (err < 0) + return err; + + open_rsp->h.length = sizeof(*open_rsp); + err = audioservice_expect(data->server.fd, &open_rsp->h, + BT_OPEN); + if (err < 0) + return err; + + err = bluetooth_a2dp_init(data, params); + if (err < 0) + return err; + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + req->h.type = BT_REQUEST; + req->h.name = BT_SET_CONFIGURATION; + req->h.length = sizeof(*req); + + memcpy(&req->codec, &a2dp->sbc_capabilities, + sizeof(a2dp->sbc_capabilities)); + + req->codec.transport = BT_CAPABILITIES_TRANSPORT_A2DP; + req->codec.length = sizeof(a2dp->sbc_capabilities); + req->h.length += req->codec.length - sizeof(req->codec); + + err = audioservice_send(data->server.fd, &req->h); + if (err < 0) + return err; + + rsp->h.length = sizeof(*rsp); + err = audioservice_expect(data->server.fd, &rsp->h, + BT_SET_CONFIGURATION); + if (err < 0) + return err; + + data->transport = BT_CAPABILITIES_TRANSPORT_A2DP; + data->link_mtu = rsp->link_mtu; + + /* Setup SBC encoder now we agree on parameters */ + bluetooth_a2dp_setup(a2dp); + + DBG("\tallocation=%u\n\tsubbands=%u\n\tblocks=%u\n\tbitpool=%u\n", + a2dp->sbc.allocation, a2dp->sbc.subbands, a2dp->sbc.blocks, + a2dp->sbc.bitpool); + + return 0; +} + +static int bluetooth_poll_descriptors(snd_pcm_ioplug_t *io, + struct pollfd *pfd, unsigned int space) +{ + struct bluetooth_data *data = io->private_data; + + assert(io); + + if (space < 1) + return 0; + + pfd[0].fd = data->stream.fd; + pfd[0].events = POLLIN; + pfd[0].revents = 0; + + return 1; +} + +static int bluetooth_poll_revents(snd_pcm_ioplug_t *io ATTRIBUTE_UNUSED, + struct pollfd *pfds, unsigned int nfds, + unsigned short *revents) +{ + assert(pfds && nfds == 1 && revents); + + *revents = pfds[0].revents; + + return 0; +} + +static int bluetooth_playback_poll_descriptors_count(snd_pcm_ioplug_t *io) +{ + return 2; +} + +static int bluetooth_playback_poll_descriptors(snd_pcm_ioplug_t *io, + struct pollfd *pfd, unsigned int space) +{ + struct bluetooth_data *data = io->private_data; + + DBG(""); + + assert(data->pipefd[0] >= 0); + + if (space < 2) + return 0; + + pfd[0].fd = data->pipefd[0]; + pfd[0].events = POLLIN; + pfd[0].revents = 0; + pfd[1].fd = data->stream.fd; + pfd[1].events = POLLERR | POLLHUP | POLLNVAL; + pfd[1].revents = 0; + + return 2; +} + +static int bluetooth_playback_poll_revents(snd_pcm_ioplug_t *io, + struct pollfd *pfds, unsigned int nfds, + unsigned short *revents) +{ + static char buf[1]; + + DBG(""); + + assert(pfds); + assert(nfds == 2); + assert(revents); + assert(pfds[0].fd >= 0); + assert(pfds[1].fd >= 0); + + if (io->state != SND_PCM_STATE_PREPARED) + if (read(pfds[0].fd, buf, 1) < 0) + SYSERR("read error: %s (%d)", strerror(errno), errno); + + if (pfds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) + io->state = SND_PCM_STATE_DISCONNECTED; + + *revents = (pfds[0].revents & POLLIN) ? POLLOUT : 0; + + return 0; +} + + +static snd_pcm_sframes_t bluetooth_hsp_read(snd_pcm_ioplug_t *io, + const snd_pcm_channel_area_t *areas, + snd_pcm_uframes_t offset, + snd_pcm_uframes_t size) +{ + struct bluetooth_data *data = io->private_data; + snd_pcm_uframes_t frames_to_write, ret; + unsigned char *buff; + unsigned int frame_size = 0; + int nrecv; + + DBG("areas->step=%u areas->first=%u offset=%lu size=%lu io->nonblock=%u", + areas->step, areas->first, offset, size, io->nonblock); + + frame_size = areas->step / 8; + + if (data->count > 0) + goto proceed; + + nrecv = recv(data->stream.fd, data->buffer, data->link_mtu, + io->nonblock ? MSG_DONTWAIT : 0); + + if (nrecv < 0) { + ret = (errno == EPIPE) ? -EIO : -errno; + goto done; + } + + if ((unsigned int) nrecv != data->link_mtu) { + ret = -EIO; + SNDERR(strerror(-ret)); + goto done; + } + + /* Increment hardware transmition pointer */ + data->hw_ptr = (data->hw_ptr + data->link_mtu / frame_size) % + io->buffer_size; + +proceed: + buff = (unsigned char *) areas->addr + + (areas->first + areas->step * offset) / 8; + + if ((data->count + size * frame_size) <= data->link_mtu) + frames_to_write = size; + else + frames_to_write = (data->link_mtu - data->count) / frame_size; + + memcpy(buff, data->buffer + data->count, frame_size * frames_to_write); + data->count += (frame_size * frames_to_write); + data->count %= data->link_mtu; + + /* Return written frames count */ + ret = frames_to_write; + +done: + DBG("returning %lu", ret); + return ret; +} + +static snd_pcm_sframes_t bluetooth_hsp_write(snd_pcm_ioplug_t *io, + const snd_pcm_channel_area_t *areas, + snd_pcm_uframes_t offset, + snd_pcm_uframes_t size) +{ + struct bluetooth_data *data = io->private_data; + snd_pcm_sframes_t ret = 0; + snd_pcm_uframes_t frames_to_read; + uint8_t *buff; + int rsend, frame_size; + + DBG("areas->step=%u areas->first=%u offset=%lu, size=%lu io->nonblock=%u", + areas->step, areas->first, offset, size, io->nonblock); + + if (io->hw_ptr > io->appl_ptr) { + ret = bluetooth_playback_stop(io); + if (ret == 0) + ret = -EPIPE; + goto done; + } + + frame_size = areas->step / 8; + if ((data->count + size * frame_size) <= data->link_mtu) + frames_to_read = size; + else + frames_to_read = (data->link_mtu - data->count) / frame_size; + + DBG("count=%d frames_to_read=%lu", data->count, frames_to_read); + + /* Ready for more data */ + buff = (uint8_t *) areas->addr + + (areas->first + areas->step * offset) / 8; + memcpy(data->buffer + data->count, buff, frame_size * frames_to_read); + + /* Remember we have some frames in the pipe now */ + data->count += frames_to_read * frame_size; + if (data->count != data->link_mtu) { + ret = frames_to_read; + goto done; + } + + rsend = send(data->stream.fd, data->buffer, data->link_mtu, + io->nonblock ? MSG_DONTWAIT : 0); + if (rsend > 0) { + /* Reset count pointer */ + data->count = 0; + + ret = frames_to_read; + } else if (rsend < 0) + ret = (errno == EPIPE) ? -EIO : -errno; + else + ret = -EIO; + +done: + DBG("returning %ld", ret); + return ret; +} + +static snd_pcm_sframes_t bluetooth_a2dp_read(snd_pcm_ioplug_t *io, + const snd_pcm_channel_area_t *areas, + snd_pcm_uframes_t offset, snd_pcm_uframes_t size) +{ + snd_pcm_uframes_t ret = 0; + return ret; +} + +static int avdtp_write(struct bluetooth_data *data) +{ + int err; + struct rtp_header *header; + struct rtp_payload *payload; + struct bluetooth_a2dp *a2dp = &data->a2dp; + + header = (void *) a2dp->buffer; + payload = (void *) (a2dp->buffer + sizeof(*header)); + + memset(a2dp->buffer, 0, sizeof(*header) + sizeof(*payload)); + + payload->frame_count = a2dp->frame_count; + header->v = 2; + header->pt = 1; + header->sequence_number = htons(a2dp->seq_num); + header->timestamp = htonl(a2dp->nsamples); + header->ssrc = htonl(1); + + err = send(data->stream.fd, a2dp->buffer, a2dp->count, MSG_DONTWAIT); + if (err < 0) { + err = -errno; + DBG("send failed: %s (%d)", strerror(-err), -err); + } + + /* Reset buffer of data to send */ + a2dp->count = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + a2dp->frame_count = 0; + a2dp->samples = 0; + a2dp->seq_num++; + + return err; +} + +static snd_pcm_sframes_t bluetooth_a2dp_write(snd_pcm_ioplug_t *io, + const snd_pcm_channel_area_t *areas, + snd_pcm_uframes_t offset, snd_pcm_uframes_t size) +{ + struct bluetooth_data *data = io->private_data; + struct bluetooth_a2dp *a2dp = &data->a2dp; + snd_pcm_sframes_t ret = 0; + unsigned int bytes_left; + int frame_size, encoded; + ssize_t written; + uint8_t *buff; + + DBG("areas->step=%u areas->first=%u offset=%lu size=%lu", + areas->step, areas->first, offset, size); + DBG("hw_ptr=%lu appl_ptr=%lu diff=%lu", io->hw_ptr, io->appl_ptr, + io->appl_ptr - io->hw_ptr); + + /* Calutate starting pointers */ + frame_size = areas->step / 8; + bytes_left = size * frame_size; + buff = (uint8_t *) areas->addr + + (areas->first + areas->step * (offset)) / 8; + + /* Check for underrun */ + if (io->hw_ptr > io->appl_ptr) { + ret = bluetooth_playback_stop(io); + if (ret == 0) + ret = -EPIPE; + data->reset = 1; + return ret; + } + + /* Check if we should autostart */ + if (io->state == SND_PCM_STATE_PREPARED) { + snd_pcm_sw_params_t *swparams; + snd_pcm_uframes_t threshold; + + snd_pcm_sw_params_malloc(&swparams); + if (!snd_pcm_sw_params_current(io->pcm, swparams) && + !snd_pcm_sw_params_get_start_threshold(swparams, + &threshold)) { + if (io->appl_ptr >= threshold) { + ret = snd_pcm_start(io->pcm); + if (ret != 0) + return ret; + } + } + + snd_pcm_sw_params_free(swparams); + } + + /* Check if we have any left over data from the last write */ + if (data->count > 0) { + unsigned int additional_bytes_needed = + a2dp->codesize - data->count; + if (additional_bytes_needed > bytes_left) + goto out; + + memcpy(data->buffer + data->count, buff, + additional_bytes_needed); + + /* Enough data to encode (sbc wants 1k blocks) */ + encoded = sbc_encode(&a2dp->sbc, data->buffer, a2dp->codesize, + a2dp->buffer + a2dp->count, + sizeof(a2dp->buffer) - a2dp->count, + &written); + if (encoded <= 0) { + DBG("Encoding error %d", encoded); + goto done; + } + + /* Increment a2dp buffers */ + a2dp->count += written; + a2dp->frame_count++; + a2dp->samples += encoded / frame_size; + a2dp->nsamples += encoded / frame_size; + + /* No space left for another frame then send */ + if (a2dp->count + written >= data->link_mtu) { + avdtp_write(data); + DBG("sending packet %d, count %d, link_mtu %u", + a2dp->seq_num, a2dp->count, + data->link_mtu); + } + + /* Increment up buff pointer to take into account + * the data processed */ + buff += additional_bytes_needed; + bytes_left -= additional_bytes_needed; + + /* Since data has been process mark it as zero */ + data->count = 0; + } + + + /* Process this buffer in full chunks */ + while (bytes_left >= a2dp->codesize) { + /* Enough data to encode (sbc wants 1k blocks) */ + encoded = sbc_encode(&a2dp->sbc, buff, a2dp->codesize, + a2dp->buffer + a2dp->count, + sizeof(a2dp->buffer) - a2dp->count, + &written); + if (encoded <= 0) { + DBG("Encoding error %d", encoded); + goto done; + } + + /* Increment up buff pointer to take into account + * the data processed */ + buff += a2dp->codesize; + bytes_left -= a2dp->codesize; + + /* Increment a2dp buffers */ + a2dp->count += written; + a2dp->frame_count++; + a2dp->samples += encoded / frame_size; + a2dp->nsamples += encoded / frame_size; + + /* No space left for another frame then send */ + if (a2dp->count + written >= data->link_mtu) { + avdtp_write(data); + DBG("sending packet %d, count %d, link_mtu %u", + a2dp->seq_num, a2dp->count, + data->link_mtu); + } + } + +out: + /* Copy the extra to our temp buffer for the next write */ + if (bytes_left > 0) { + memcpy(data->buffer + data->count, buff, bytes_left); + data->count += bytes_left; + bytes_left = 0; + } + +done: + DBG("returning %ld", size - bytes_left / frame_size); + + return size - bytes_left / frame_size; +} + +static int bluetooth_playback_delay(snd_pcm_ioplug_t *io, + snd_pcm_sframes_t *delayp) +{ + DBG(""); + + /* This updates io->hw_ptr value using pointer() function */ + snd_pcm_hwsync(io->pcm); + + *delayp = io->appl_ptr - io->hw_ptr; + if ((io->state == SND_PCM_STATE_RUNNING) && (*delayp < 0)) { + io->callback->stop(io); + io->state = SND_PCM_STATE_XRUN; + *delayp = 0; + } + + /* This should never fail, ALSA API is really not + prepared to handle a non zero return value */ + return 0; +} + +static snd_pcm_ioplug_callback_t bluetooth_hsp_playback = { + .start = bluetooth_playback_start, + .stop = bluetooth_playback_stop, + .pointer = bluetooth_pointer, + .close = bluetooth_close, + .hw_params = bluetooth_hsp_hw_params, + .prepare = bluetooth_prepare, + .transfer = bluetooth_hsp_write, + .poll_descriptors_count = bluetooth_playback_poll_descriptors_count, + .poll_descriptors = bluetooth_playback_poll_descriptors, + .poll_revents = bluetooth_playback_poll_revents, + .delay = bluetooth_playback_delay, +}; + +static snd_pcm_ioplug_callback_t bluetooth_hsp_capture = { + .start = bluetooth_start, + .stop = bluetooth_stop, + .pointer = bluetooth_pointer, + .close = bluetooth_close, + .hw_params = bluetooth_hsp_hw_params, + .prepare = bluetooth_prepare, + .transfer = bluetooth_hsp_read, + .poll_descriptors = bluetooth_poll_descriptors, + .poll_revents = bluetooth_poll_revents, +}; + +static snd_pcm_ioplug_callback_t bluetooth_a2dp_playback = { + .start = bluetooth_playback_start, + .stop = bluetooth_playback_stop, + .pointer = bluetooth_pointer, + .close = bluetooth_close, + .hw_params = bluetooth_a2dp_hw_params, + .prepare = bluetooth_prepare, + .transfer = bluetooth_a2dp_write, + .poll_descriptors_count = bluetooth_playback_poll_descriptors_count, + .poll_descriptors = bluetooth_playback_poll_descriptors, + .poll_revents = bluetooth_playback_poll_revents, + .delay = bluetooth_playback_delay, +}; + +static snd_pcm_ioplug_callback_t bluetooth_a2dp_capture = { + .start = bluetooth_start, + .stop = bluetooth_stop, + .pointer = bluetooth_pointer, + .close = bluetooth_close, + .hw_params = bluetooth_a2dp_hw_params, + .prepare = bluetooth_prepare, + .transfer = bluetooth_a2dp_read, + .poll_descriptors = bluetooth_poll_descriptors, + .poll_revents = bluetooth_poll_revents, +}; + +#define ARRAY_NELEMS(a) (sizeof((a)) / sizeof((a)[0])) + +static int bluetooth_hsp_hw_constraint(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + snd_pcm_access_t access_list[] = { + SND_PCM_ACCESS_RW_INTERLEAVED, + /* Mmap access is really useless fo this driver, but we + * support it because some pieces of software out there + * insist on using it */ + SND_PCM_ACCESS_MMAP_INTERLEAVED + }; + unsigned int format_list[] = { + SND_PCM_FORMAT_S16 + }; + int err; + + /* access type */ + err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_ACCESS, + ARRAY_NELEMS(access_list), access_list); + if (err < 0) + return err; + + /* supported formats */ + err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_FORMAT, + ARRAY_NELEMS(format_list), format_list); + if (err < 0) + return err; + + /* supported channels */ + err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_CHANNELS, + 1, 1); + if (err < 0) + return err; + + /* supported rate */ + err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_RATE, + 8000, 8000); + if (err < 0) + return err; + + /* supported block size */ + err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_PERIOD_BYTES, + data->link_mtu, data->link_mtu); + if (err < 0) + return err; + + err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_PERIODS, + 2, 200); + if (err < 0) + return err; + + return 0; +} + +static int bluetooth_a2dp_hw_constraint(snd_pcm_ioplug_t *io) +{ + struct bluetooth_data *data = io->private_data; + struct bluetooth_a2dp *a2dp = &data->a2dp; + struct bluetooth_alsa_config *cfg = &data->alsa_config; + snd_pcm_access_t access_list[] = { + SND_PCM_ACCESS_RW_INTERLEAVED, + /* Mmap access is really useless fo this driver, but we + * support it because some pieces of software out there + * insist on using it */ + SND_PCM_ACCESS_MMAP_INTERLEAVED + }; + unsigned int format_list[] = { + SND_PCM_FORMAT_S16 + }; + unsigned int rate_list[4]; + unsigned int rate_count; + int err, min_channels, max_channels; + unsigned int period_list[] = { + 2048, + 4096, /* e.g. 23.2msec/period (stereo 16bit at 44.1kHz) */ + 8192 + }; + + /* access type */ + err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_ACCESS, + ARRAY_NELEMS(access_list), access_list); + if (err < 0) + return err; + + /* supported formats */ + err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_FORMAT, + ARRAY_NELEMS(format_list), format_list); + if (err < 0) + return err; + + /* supported channels */ + if (cfg->has_channel_mode) + a2dp->sbc_capabilities.channel_mode = cfg->channel_mode; + + if (a2dp->sbc_capabilities.channel_mode & + BT_A2DP_CHANNEL_MODE_MONO) + min_channels = 1; + else + min_channels = 2; + + if (a2dp->sbc_capabilities.channel_mode & + (~BT_A2DP_CHANNEL_MODE_MONO)) + max_channels = 2; + else + max_channels = 1; + + err = snd_pcm_ioplug_set_param_minmax(io, SND_PCM_IOPLUG_HW_CHANNELS, + min_channels, max_channels); + if (err < 0) + return err; + + /* supported buffer sizes + * (can be used as 3*8192, 6*4096, 12*2048, ...) */ + err = snd_pcm_ioplug_set_param_minmax(io, + SND_PCM_IOPLUG_HW_BUFFER_BYTES, + 8192*3, 8192*3); + if (err < 0) + return err; + + /* supported block sizes: */ + err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_PERIOD_BYTES, + ARRAY_NELEMS(period_list), period_list); + if (err < 0) + return err; + + /* supported rates */ + rate_count = 0; + if (cfg->has_rate) { + rate_list[rate_count] = cfg->rate; + rate_count++; + } else { + if (a2dp->sbc_capabilities.frequency & + BT_SBC_SAMPLING_FREQ_16000) { + rate_list[rate_count] = 16000; + rate_count++; + } + + if (a2dp->sbc_capabilities.frequency & + BT_SBC_SAMPLING_FREQ_32000) { + rate_list[rate_count] = 32000; + rate_count++; + } + + if (a2dp->sbc_capabilities.frequency & + BT_SBC_SAMPLING_FREQ_44100) { + rate_list[rate_count] = 44100; + rate_count++; + } + + if (a2dp->sbc_capabilities.frequency & + BT_SBC_SAMPLING_FREQ_48000) { + rate_list[rate_count] = 48000; + rate_count++; + } + } + + err = snd_pcm_ioplug_set_param_list(io, SND_PCM_IOPLUG_HW_RATE, + rate_count, rate_list); + if (err < 0) + return err; + + return 0; +} + +static int bluetooth_parse_config(snd_config_t *conf, + struct bluetooth_alsa_config *bt_config) +{ + snd_config_iterator_t i, next; + + memset(bt_config, 0, sizeof(struct bluetooth_alsa_config)); + + /* Set defaults */ + bt_config->autoconnect = 1; + + snd_config_for_each(i, next, conf) { + snd_config_t *n = snd_config_iterator_entry(i); + const char *id, *value; + + if (snd_config_get_id(n, &id) < 0) + continue; + + if (strcmp(id, "comment") == 0 || strcmp(id, "type") == 0) + continue; + + if (strcmp(id, "autoconnect") == 0) { + int b; + + b = snd_config_get_bool(n); + if (b < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + bt_config->autoconnect = b; + continue; + } + + if (strcmp(id, "device") == 0 || strcmp(id, "bdaddr") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + bt_config->has_device = 1; + strncpy(bt_config->device, value, 18); + continue; + } + + if (strcmp(id, "profile") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + if (strcmp(value, "auto") == 0) { + bt_config->transport = BT_CAPABILITIES_TRANSPORT_ANY; + bt_config->has_transport = 1; + } else if (strcmp(value, "voice") == 0 || + strcmp(value, "hfp") == 0) { + bt_config->transport = BT_CAPABILITIES_TRANSPORT_SCO; + bt_config->has_transport = 1; + } else if (strcmp(value, "hifi") == 0 || + strcmp(value, "a2dp") == 0) { + bt_config->transport = BT_CAPABILITIES_TRANSPORT_A2DP; + bt_config->has_transport = 1; + } + continue; + } + + if (strcmp(id, "rate") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + bt_config->rate = atoi(value); + bt_config->has_rate = 1; + continue; + } + + if (strcmp(id, "mode") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + if (strcmp(value, "mono") == 0) { + bt_config->channel_mode = BT_A2DP_CHANNEL_MODE_MONO; + bt_config->has_channel_mode = 1; + } else if (strcmp(value, "dual") == 0) { + bt_config->channel_mode = BT_A2DP_CHANNEL_MODE_DUAL_CHANNEL; + bt_config->has_channel_mode = 1; + } else if (strcmp(value, "stereo") == 0) { + bt_config->channel_mode = BT_A2DP_CHANNEL_MODE_STEREO; + bt_config->has_channel_mode = 1; + } else if (strcmp(value, "joint") == 0) { + bt_config->channel_mode = BT_A2DP_CHANNEL_MODE_JOINT_STEREO; + bt_config->has_channel_mode = 1; + } + continue; + } + + if (strcmp(id, "allocation") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + if (strcmp(value, "loudness") == 0) { + bt_config->allocation_method = BT_A2DP_ALLOCATION_LOUDNESS; + bt_config->has_allocation_method = 1; + } else if (strcmp(value, "snr") == 0) { + bt_config->allocation_method = BT_A2DP_ALLOCATION_SNR; + bt_config->has_allocation_method = 1; + } + continue; + } + + if (strcmp(id, "subbands") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + bt_config->subbands = atoi(value); + bt_config->has_subbands = 1; + continue; + } + + if (strcmp(id, "blocks") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + bt_config->block_length = atoi(value); + bt_config->has_block_length = 1; + continue; + } + + if (strcmp(id, "bitpool") == 0) { + if (snd_config_get_string(n, &value) < 0) { + SNDERR("Invalid type for %s", id); + return -EINVAL; + } + + bt_config->bitpool = atoi(value); + bt_config->has_bitpool = 1; + continue; + } + + SNDERR("Unknown field %s", id); + return -EINVAL; + } + + return 0; +} + +static int audioservice_send(int sk, const bt_audio_msg_header_t *msg) +{ + int err; + uint16_t length; + + length = msg->length ? msg->length : BT_SUGGESTED_BUFFER_SIZE; + + DBG("sending %s:%s", bt_audio_strtype(msg->type), + bt_audio_strname(msg->name)); + if (send(sk, msg, length, 0) > 0) + err = 0; + else { + err = -errno; + SNDERR("Error sending data to audio service: %s(%d)", + strerror(-err), -err); + } + + return err; +} + +static int audioservice_recv(int sk, bt_audio_msg_header_t *inmsg) +{ + int err; + ssize_t ret; + const char *type, *name; + uint16_t length; + + length = inmsg->length ? inmsg->length : BT_SUGGESTED_BUFFER_SIZE; + + DBG("trying to receive msg from audio service..."); + + ret = recv(sk, inmsg, length, 0); + if (ret < 0) { + err = -errno; + SNDERR("Error receiving IPC data from bluetoothd: %s (%d)", + strerror(-err), -err); + } else if ((size_t) ret < sizeof(bt_audio_msg_header_t)) { + SNDERR("Too short (%d bytes) IPC packet from bluetoothd", ret); + err = -EINVAL; + } else { + type = bt_audio_strtype(inmsg->type); + name = bt_audio_strname(inmsg->name); + if (type && name) { + DBG("Received %s - %s", type, name); + err = 0; + } else { + err = -EINVAL; + SNDERR("Bogus message type %d - name %d" + " received from audio service", + inmsg->type, inmsg->name); + } + + } + + return err; +} + +static int audioservice_expect(int sk, bt_audio_msg_header_t *rsp, + int expected_name) +{ + bt_audio_error_t *error; + int err = audioservice_recv(sk, rsp); + + if (err != 0) + return err; + + if (rsp->name != expected_name) { + err = -EINVAL; + SNDERR("Bogus message %s received while %s was expected", + bt_audio_strname(rsp->name), + bt_audio_strname(expected_name)); + } + + if (rsp->type == BT_ERROR) { + error = (void *) rsp; + SNDERR("%s failed : %s(%d)", + bt_audio_strname(rsp->name), + strerror(error->posix_errno), + error->posix_errno); + return -error->posix_errno; + } + + return err; +} + +static int bluetooth_parse_capabilities(struct bluetooth_data *data, + struct bt_get_capabilities_rsp *rsp) +{ + int bytes_left = rsp->h.length - sizeof(*rsp); + codec_capabilities_t *codec = (void *) rsp->data; + + data->transport = codec->transport; + + if (codec->transport != BT_CAPABILITIES_TRANSPORT_A2DP) + return 0; + + while (bytes_left > 0) { + if ((codec->type == BT_A2DP_SBC_SINK) && + !(codec->lock & BT_WRITE_LOCK)) + break; + + bytes_left -= codec->length; + codec = (void *) codec + codec->length; + } + + if (bytes_left <= 0 || + codec->length != sizeof(data->a2dp.sbc_capabilities)) + return -EINVAL; + + memcpy(&data->a2dp.sbc_capabilities, codec, codec->length); + + return 0; +} + +static int bluetooth_init(struct bluetooth_data *data, + snd_pcm_stream_t stream, snd_config_t *conf) +{ + int sk, err; + struct bluetooth_alsa_config *alsa_conf = &data->alsa_config; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_get_capabilities_req *req = (void *) buf; + struct bt_get_capabilities_rsp *rsp = (void *) buf; + + memset(data, 0, sizeof(struct bluetooth_data)); + + err = bluetooth_parse_config(conf, alsa_conf); + if (err < 0) + return err; + + data->server.fd = -1; + data->stream.fd = -1; + + sk = bt_audio_service_open(); + if (sk < 0) { + err = -errno; + goto failed; + } + + data->server.fd = sk; + data->server.events = POLLIN; + + data->pipefd[0] = -1; + data->pipefd[1] = -1; + + if (pipe(data->pipefd) < 0) { + err = -errno; + goto failed; + } + if (fcntl(data->pipefd[0], F_SETFL, O_NONBLOCK) < 0) { + err = -errno; + goto failed; + } + if (fcntl(data->pipefd[1], F_SETFL, O_NONBLOCK) < 0) { + err = -errno; + goto failed; + } + + memset(req, 0, BT_SUGGESTED_BUFFER_SIZE); + req->h.type = BT_REQUEST; + req->h.name = BT_GET_CAPABILITIES; + req->h.length = sizeof(*req); + + if (alsa_conf->autoconnect) + req->flags |= BT_FLAG_AUTOCONNECT; + strncpy(req->destination, alsa_conf->device, 18); + if (alsa_conf->has_transport) + req->transport = alsa_conf->transport; + else + req->transport = BT_CAPABILITIES_TRANSPORT_ANY; + + err = audioservice_send(data->server.fd, &req->h); + if (err < 0) + goto failed; + + rsp->h.length = 0; + err = audioservice_expect(data->server.fd, &rsp->h, + BT_GET_CAPABILITIES); + if (err < 0) + goto failed; + + bluetooth_parse_capabilities(data, rsp); + + return 0; + +failed: + if (sk >= 0) + bt_audio_service_close(sk); + return err; +} + +SND_PCM_PLUGIN_DEFINE_FUNC(bluetooth); + +SND_PCM_PLUGIN_DEFINE_FUNC(bluetooth) +{ + struct bluetooth_data *data; + int err; + + DBG("Bluetooth PCM plugin (%s)", + stream == SND_PCM_STREAM_PLAYBACK ? "Playback" : "Capture"); + + data = malloc(sizeof(struct bluetooth_data)); + if (!data) { + err = -ENOMEM; + goto error; + } + + err = bluetooth_init(data, stream, conf); + if (err < 0) + goto error; + + data->io.version = SND_PCM_IOPLUG_VERSION; + data->io.name = "Bluetooth Audio Device"; + data->io.mmap_rw = 0; /* No direct mmap communication */ + data->io.private_data = data; + + if (data->transport == BT_CAPABILITIES_TRANSPORT_A2DP) + data->io.callback = stream == SND_PCM_STREAM_PLAYBACK ? + &bluetooth_a2dp_playback : + &bluetooth_a2dp_capture; + else + data->io.callback = stream == SND_PCM_STREAM_PLAYBACK ? + &bluetooth_hsp_playback : + &bluetooth_hsp_capture; + + err = snd_pcm_ioplug_create(&data->io, name, stream, mode); + if (err < 0) + goto error; + + if (data->transport == BT_CAPABILITIES_TRANSPORT_A2DP) + err = bluetooth_a2dp_hw_constraint(&data->io); + else + err = bluetooth_hsp_hw_constraint(&data->io); + + if (err < 0) { + snd_pcm_ioplug_delete(&data->io); + goto error; + } + + *pcmp = data->io.pcm; + + return 0; + +error: + if (data) + bluetooth_exit(data); + + return err; +} + +SND_PCM_PLUGIN_SYMBOL(bluetooth); diff --git a/audio/rtp.h b/audio/rtp.h new file mode 100644 index 0000000..45fddcf --- /dev/null +++ b/audio/rtp.h @@ -0,0 +1,76 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +struct rtp_header { + unsigned cc:4; + unsigned x:1; + unsigned p:1; + unsigned v:2; + + unsigned pt:7; + unsigned m:1; + + uint16_t sequence_number; + uint32_t timestamp; + uint32_t ssrc; + uint32_t csrc[0]; +} __attribute__ ((packed)); + +struct rtp_payload { + unsigned frame_count:4; + unsigned rfa0:1; + unsigned is_last_fragment:1; + unsigned is_first_fragment:1; + unsigned is_fragmented:1; +} __attribute__ ((packed)); + +#elif __BYTE_ORDER == __BIG_ENDIAN + +struct rtp_header { + unsigned v:2; + unsigned p:1; + unsigned x:1; + unsigned cc:4; + + unsigned m:1; + unsigned pt:7; + + uint16_t sequence_number; + uint32_t timestamp; + uint32_t ssrc; + uint32_t csrc[0]; +} __attribute__ ((packed)); + +struct rtp_payload { + unsigned is_fragmented:1; + unsigned is_first_fragment:1; + unsigned is_last_fragment:1; + unsigned rfa0:1; + unsigned frame_count:4; +} __attribute__ ((packed)); + +#else +#error "Unknown byte order" +#endif diff --git a/audio/sink.c b/audio/sink.c new file mode 100644 index 0000000..8ba4e2a --- /dev/null +++ b/audio/sink.c @@ -0,0 +1,743 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include +#include + +#include +#include +#include + +#include "log.h" + +#include "device.h" +#include "avdtp.h" +#include "media.h" +#include "a2dp.h" +#include "error.h" +#include "sink.h" +#include "dbus-common.h" +#include "../src/adapter.h" +#include "../src/device.h" + +#define STREAM_SETUP_RETRY_TIMER 2 + +struct pending_request { + DBusConnection *conn; + DBusMessage *msg; + unsigned int id; +}; + +struct sink { + struct audio_device *dev; + struct avdtp *session; + struct avdtp_stream *stream; + unsigned int cb_id; + guint retry_id; + avdtp_session_state_t session_state; + avdtp_state_t stream_state; + sink_state_t state; + struct pending_request *connect; + struct pending_request *disconnect; + DBusConnection *conn; +}; + +struct sink_state_callback { + sink_state_cb cb; + void *user_data; + unsigned int id; +}; + +static GSList *sink_callbacks = NULL; + +static unsigned int avdtp_callback_id = 0; + +static char *str_state[] = { + "SINK_STATE_DISCONNECTED", + "SINK_STATE_CONNECTING", + "SINK_STATE_CONNECTED", + "SINK_STATE_PLAYING", +}; + +static const char *state2str(sink_state_t state) +{ + switch (state) { + case SINK_STATE_DISCONNECTED: + return "disconnected"; + case SINK_STATE_CONNECTING: + return "connecting"; + case SINK_STATE_CONNECTED: + return "connected"; + case SINK_STATE_PLAYING: + return "playing"; + default: + error("Invalid sink state %d", state); + return NULL; + } +} + +static void sink_set_state(struct audio_device *dev, sink_state_t new_state) +{ + struct sink *sink = dev->sink; + const char *state_str; + sink_state_t old_state = sink->state; + GSList *l; + + sink->state = new_state; + + state_str = state2str(new_state); + if (state_str) + emit_property_changed(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); + + DBG("State changed %s: %s -> %s", dev->path, str_state[old_state], + str_state[new_state]); + + for (l = sink_callbacks; l != NULL; l = l->next) { + struct sink_state_callback *cb = l->data; + cb->cb(dev, old_state, new_state, cb->user_data); + } +} + +static void avdtp_state_callback(struct audio_device *dev, + struct avdtp *session, + avdtp_session_state_t old_state, + avdtp_session_state_t new_state, + void *user_data) +{ + struct sink *sink = dev->sink; + + if (sink == NULL) + return; + + switch (new_state) { + case AVDTP_SESSION_STATE_DISCONNECTED: + if (sink->state != SINK_STATE_CONNECTING) { + gboolean value = FALSE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, "Disconnected", + DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, "Connected", + DBUS_TYPE_BOOLEAN, &value); + } + sink_set_state(dev, SINK_STATE_DISCONNECTED); + break; + case AVDTP_SESSION_STATE_CONNECTING: + sink_set_state(dev, SINK_STATE_CONNECTING); + break; + case AVDTP_SESSION_STATE_CONNECTED: + break; + } + + sink->session_state = new_state; +} + +static void pending_request_free(struct audio_device *dev, + struct pending_request *pending) +{ + if (pending->conn) + dbus_connection_unref(pending->conn); + if (pending->msg) + dbus_message_unref(pending->msg); + if (pending->id) + a2dp_cancel(dev, pending->id); + + g_free(pending); +} + +static void stream_state_changed(struct avdtp_stream *stream, + avdtp_state_t old_state, + avdtp_state_t new_state, + struct avdtp_error *err, + void *user_data) +{ + struct audio_device *dev = user_data; + struct sink *sink = dev->sink; + gboolean value; + + if (err) + return; + + switch (new_state) { + case AVDTP_STATE_IDLE: + if (sink->disconnect) { + DBusMessage *reply; + struct pending_request *p; + + p = sink->disconnect; + sink->disconnect = NULL; + + reply = dbus_message_new_method_return(p->msg); + g_dbus_send_message(p->conn, reply); + pending_request_free(dev, p); + } + + if (sink->session) { + avdtp_unref(sink->session); + sink->session = NULL; + } + sink->stream = NULL; + sink->cb_id = 0; + break; + case AVDTP_STATE_OPEN: + if (old_state == AVDTP_STATE_CONFIGURED && + sink->state == SINK_STATE_CONNECTING) { + value = TRUE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, + "Connected", + DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, + "Connected", + DBUS_TYPE_BOOLEAN, &value); + } else if (old_state == AVDTP_STATE_STREAMING) { + value = FALSE; + g_dbus_emit_signal(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, + "Stopped", + DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, + "Playing", + DBUS_TYPE_BOOLEAN, &value); + } + sink_set_state(dev, SINK_STATE_CONNECTED); + break; + case AVDTP_STATE_STREAMING: + value = TRUE; + g_dbus_emit_signal(dev->conn, dev->path, AUDIO_SINK_INTERFACE, + "Playing", DBUS_TYPE_INVALID); + emit_property_changed(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, "Playing", + DBUS_TYPE_BOOLEAN, &value); + sink_set_state(dev, SINK_STATE_PLAYING); + break; + case AVDTP_STATE_CONFIGURED: + case AVDTP_STATE_CLOSING: + case AVDTP_STATE_ABORTING: + default: + break; + } + + sink->stream_state = new_state; +} + +static void error_failed(DBusConnection *conn, DBusMessage *msg, + const char *desc) +{ + DBusMessage *reply = btd_error_failed(msg, desc); + g_dbus_send_message(conn, reply); +} + +static gboolean stream_setup_retry(gpointer user_data) +{ + struct sink *sink = user_data; + struct pending_request *pending = sink->connect; + + sink->retry_id = 0; + + if (sink->stream_state >= AVDTP_STATE_OPEN) { + DBG("Stream successfully created, after XCASE connect:connect"); + if (pending->msg) { + DBusMessage *reply; + reply = dbus_message_new_method_return(pending->msg); + g_dbus_send_message(pending->conn, reply); + } + } else { + DBG("Stream setup failed, after XCASE connect:connect"); + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + } + + sink->connect = NULL; + pending_request_free(sink->dev, pending); + + return FALSE; +} + +static void stream_setup_complete(struct avdtp *session, struct a2dp_sep *sep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data) +{ + struct sink *sink = user_data; + struct pending_request *pending; + + pending = sink->connect; + + pending->id = 0; + + if (stream) { + DBG("Stream successfully created"); + + if (pending->msg) { + DBusMessage *reply; + reply = dbus_message_new_method_return(pending->msg); + g_dbus_send_message(pending->conn, reply); + } + + sink->connect = NULL; + pending_request_free(sink->dev, pending); + + return; + } + + avdtp_unref(sink->session); + sink->session = NULL; + if (avdtp_error_category(err) == AVDTP_ERRNO + && avdtp_error_posix_errno(err) != EHOSTDOWN) { + DBG("connect:connect XCASE detected"); + sink->retry_id = g_timeout_add_seconds(STREAM_SETUP_RETRY_TIMER, + stream_setup_retry, + sink); + } else { + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + sink->connect = NULL; + pending_request_free(sink->dev, pending); + DBG("Stream setup failed : %s", avdtp_strerror(err)); + } +} + +static void select_complete(struct avdtp *session, struct a2dp_sep *sep, + GSList *caps, void *user_data) +{ + struct sink *sink = user_data; + struct pending_request *pending; + int id; + + pending = sink->connect; + pending->id = 0; + + id = a2dp_config(session, sep, stream_setup_complete, caps, sink); + if (id == 0) + goto failed; + + pending->id = id; + return; + +failed: + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + pending_request_free(sink->dev, pending); + sink->connect = NULL; + avdtp_unref(sink->session); + sink->session = NULL; +} + +static void discovery_complete(struct avdtp *session, GSList *seps, struct avdtp_error *err, + void *user_data) +{ + struct sink *sink = user_data; + struct pending_request *pending; + int id; + + if (!sink->connect) { + avdtp_unref(sink->session); + sink->session = NULL; + return; + } + + pending = sink->connect; + + if (err) { + avdtp_unref(sink->session); + sink->session = NULL; + if (avdtp_error_category(err) == AVDTP_ERRNO + && avdtp_error_posix_errno(err) != EHOSTDOWN) { + DBG("connect:connect XCASE detected"); + sink->retry_id = + g_timeout_add_seconds(STREAM_SETUP_RETRY_TIMER, + stream_setup_retry, + sink); + } else + goto failed; + return; + } + + DBG("Discovery complete"); + + id = a2dp_select_capabilities(sink->session, AVDTP_SEP_TYPE_SINK, NULL, + select_complete, sink); + if (id == 0) + goto failed; + + pending->id = id; + return; + +failed: + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + pending_request_free(sink->dev, pending); + sink->connect = NULL; + avdtp_unref(sink->session); + sink->session = NULL; +} + +gboolean sink_setup_stream(struct sink *sink, struct avdtp *session) +{ + if (sink->connect || sink->disconnect) + return FALSE; + + if (session && !sink->session) + sink->session = avdtp_ref(session); + + if (!sink->session) + return FALSE; + + if (avdtp_discover(sink->session, discovery_complete, sink) < 0) + return FALSE; + + sink->connect = g_new0(struct pending_request, 1); + + return TRUE; +} + +static DBusMessage *sink_connect(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *dev = data; + struct sink *sink = dev->sink; + struct pending_request *pending; + + if (!sink->session) + sink->session = avdtp_get(&dev->src, &dev->dst); + + if (!sink->session) + return btd_error_failed(msg, "Unable to get a session"); + + if (sink->connect || sink->disconnect) + return btd_error_busy(msg); + + if (sink->stream_state >= AVDTP_STATE_OPEN) + return btd_error_already_connected(msg); + + if (!sink_setup_stream(sink, NULL)) + return btd_error_failed(msg, "Failed to create a stream"); + + dev->auto_connect = FALSE; + + pending = sink->connect; + + pending->conn = dbus_connection_ref(conn); + pending->msg = dbus_message_ref(msg); + + DBG("stream creation in progress"); + + return NULL; +} + +static DBusMessage *sink_disconnect(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + struct sink *sink = device->sink; + struct pending_request *pending; + int err; + + if (!sink->session) + return btd_error_not_connected(msg); + + if (sink->connect || sink->disconnect) + return btd_error_busy(msg); + + if (sink->stream_state < AVDTP_STATE_OPEN) { + DBusMessage *reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + avdtp_unref(sink->session); + sink->session = NULL; + return reply; + } + + err = avdtp_close(sink->session, sink->stream, FALSE); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + pending = g_new0(struct pending_request, 1); + pending->conn = dbus_connection_ref(conn); + pending->msg = dbus_message_ref(msg); + sink->disconnect = pending; + + return NULL; +} + +static DBusMessage *sink_is_connected(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + struct audio_device *device = data; + struct sink *sink = device->sink; + DBusMessage *reply; + dbus_bool_t connected; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + connected = (sink->stream_state >= AVDTP_STATE_CONFIGURED); + + dbus_message_append_args(reply, DBUS_TYPE_BOOLEAN, &connected, + DBUS_TYPE_INVALID); + + return reply; +} + +static DBusMessage *sink_get_properties(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + struct sink *sink = device->sink; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + const char *state; + gboolean value; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + /* Playing */ + value = (sink->stream_state == AVDTP_STATE_STREAMING); + dict_append_entry(&dict, "Playing", DBUS_TYPE_BOOLEAN, &value); + + /* Connected */ + value = (sink->stream_state >= AVDTP_STATE_CONFIGURED); + dict_append_entry(&dict, "Connected", DBUS_TYPE_BOOLEAN, &value); + + /* State */ + state = state2str(sink->state); + if (state) + dict_append_entry(&dict, "State", DBUS_TYPE_STRING, &state); + + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static const GDBusMethodTable sink_methods[] = { + { GDBUS_ASYNC_METHOD("Connect", NULL, NULL, sink_connect) }, + { GDBUS_ASYNC_METHOD("Disconnect", NULL, NULL, sink_disconnect) }, + { GDBUS_DEPRECATED_METHOD("IsConnected", + NULL, GDBUS_ARGS({ "connected", "b" }), + sink_is_connected) }, + { GDBUS_METHOD("GetProperties", + NULL, GDBUS_ARGS({ "properties", "a{sv}" }), + sink_get_properties) }, + { } +}; + +static const GDBusSignalTable sink_signals[] = { + { GDBUS_DEPRECATED_SIGNAL("Connected", NULL) }, + { GDBUS_DEPRECATED_SIGNAL("Disconnected", NULL) }, + { GDBUS_DEPRECATED_SIGNAL("Playing", NULL) }, + { GDBUS_DEPRECATED_SIGNAL("Stopped", NULL) }, + { GDBUS_SIGNAL("PropertyChanged", + GDBUS_ARGS({ "name", "s" }, { "value", "v" })) }, + { } +}; + +static void sink_free(struct audio_device *dev) +{ + struct sink *sink = dev->sink; + + if (sink->cb_id) + avdtp_stream_remove_cb(sink->session, sink->stream, + sink->cb_id); + + if (sink->session) + avdtp_unref(sink->session); + + if (sink->connect) + pending_request_free(dev, sink->connect); + + if (sink->disconnect) + pending_request_free(dev, sink->disconnect); + + if (sink->retry_id) + g_source_remove(sink->retry_id); + + g_free(sink); + dev->sink = NULL; +} + +static void path_unregister(void *data) +{ + struct audio_device *dev = data; + + DBG("Unregistered interface %s on path %s", + AUDIO_SINK_INTERFACE, dev->path); + + sink_free(dev); +} + +void sink_unregister(struct audio_device *dev) +{ + g_dbus_unregister_interface(dev->conn, dev->path, + AUDIO_SINK_INTERFACE); +} + +struct sink *sink_init(struct audio_device *dev) +{ + struct sink *sink; + + if (!g_dbus_register_interface(dev->conn, dev->path, + AUDIO_SINK_INTERFACE, + sink_methods, sink_signals, NULL, + dev, path_unregister)) + return NULL; + + DBG("Registered interface %s on path %s", + AUDIO_SINK_INTERFACE, dev->path); + + if (avdtp_callback_id == 0) + avdtp_callback_id = avdtp_add_state_cb(avdtp_state_callback, + NULL); + + sink = g_new0(struct sink, 1); + + sink->dev = dev; + + return sink; +} + +gboolean sink_is_active(struct audio_device *dev) +{ + struct sink *sink = dev->sink; + + if (sink->session) + return TRUE; + + return FALSE; +} + +sink_state_t sink_get_state(struct audio_device *dev) +{ + struct sink *sink = dev->sink; + + return sink->state; +} + +gboolean sink_new_stream(struct audio_device *dev, struct avdtp *session, + struct avdtp_stream *stream) +{ + struct sink *sink = dev->sink; + + if (sink->stream) + return FALSE; + + if (!sink->session) + sink->session = avdtp_ref(session); + + sink->stream = stream; + + sink->cb_id = avdtp_stream_add_cb(session, stream, + stream_state_changed, dev); + + return TRUE; +} + +gboolean sink_shutdown(struct sink *sink) +{ + if (!sink->session) + return FALSE; + + avdtp_set_device_disconnect(sink->session, TRUE); + + /* cancel pending connect */ + if (sink->connect) { + struct pending_request *pending = sink->connect; + + if (pending->msg) + error_failed(pending->conn, pending->msg, + "Stream setup failed"); + pending_request_free(sink->dev, pending); + sink->connect = NULL; + + avdtp_unref(sink->session); + sink->session = NULL; + + return TRUE; + } + + /* disconnect already ongoing */ + if (sink->disconnect) + return TRUE; + + if (!sink->stream) + return FALSE; + + if (avdtp_close(sink->session, sink->stream, FALSE) < 0) + return FALSE; + + return TRUE; +} + +unsigned int sink_add_state_cb(sink_state_cb cb, void *user_data) +{ + struct sink_state_callback *state_cb; + static unsigned int id = 0; + + state_cb = g_new(struct sink_state_callback, 1); + state_cb->cb = cb; + state_cb->user_data = user_data; + state_cb->id = ++id; + + sink_callbacks = g_slist_append(sink_callbacks, state_cb); + + return state_cb->id; +} + +gboolean sink_remove_state_cb(unsigned int id) +{ + GSList *l; + + for (l = sink_callbacks; l != NULL; l = l->next) { + struct sink_state_callback *cb = l->data; + if (cb && cb->id == id) { + sink_callbacks = g_slist_remove(sink_callbacks, cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} diff --git a/audio/sink.h b/audio/sink.h new file mode 100644 index 0000000..4fea5ba --- /dev/null +++ b/audio/sink.h @@ -0,0 +1,49 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define AUDIO_SINK_INTERFACE "org.bluez.AudioSink" + +typedef enum { + SINK_STATE_DISCONNECTED, + SINK_STATE_CONNECTING, + SINK_STATE_CONNECTED, + SINK_STATE_PLAYING, +} sink_state_t; + +typedef void (*sink_state_cb) (struct audio_device *dev, + sink_state_t old_state, + sink_state_t new_state, + void *user_data); + +unsigned int sink_add_state_cb(sink_state_cb cb, void *user_data); +gboolean sink_remove_state_cb(unsigned int id); + +struct sink *sink_init(struct audio_device *dev); +void sink_unregister(struct audio_device *dev); +gboolean sink_is_active(struct audio_device *dev); +sink_state_t sink_get_state(struct audio_device *dev); +gboolean sink_new_stream(struct audio_device *dev, struct avdtp *session, + struct avdtp_stream *stream); +gboolean sink_setup_stream(struct sink *sink, struct avdtp *session); +gboolean sink_shutdown(struct sink *sink); diff --git a/audio/source.c b/audio/source.c new file mode 100644 index 0000000..a5fa859 --- /dev/null +++ b/audio/source.c @@ -0,0 +1,632 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * Copyright (C) 2009 Joao Paulo Rechi Vita + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include +#include + +#include +#include +#include + +#include "log.h" + +#include "device.h" +#include "avdtp.h" +#include "media.h" +#include "a2dp.h" +#include "error.h" +#include "source.h" +#include "dbus-common.h" +#include "../src/adapter.h" +#include "../src/device.h" + +#define STREAM_SETUP_RETRY_TIMER 2 + +struct pending_request { + DBusConnection *conn; + DBusMessage *msg; + unsigned int id; +}; + +struct source { + struct audio_device *dev; + struct avdtp *session; + struct avdtp_stream *stream; + unsigned int cb_id; + guint retry_id; + avdtp_session_state_t session_state; + avdtp_state_t stream_state; + source_state_t state; + struct pending_request *connect; + struct pending_request *disconnect; + DBusConnection *conn; +}; + +struct source_state_callback { + source_state_cb cb; + void *user_data; + unsigned int id; +}; + +static GSList *source_callbacks = NULL; + +static unsigned int avdtp_callback_id = 0; + +static const char *state2str(source_state_t state) +{ + switch (state) { + case SOURCE_STATE_DISCONNECTED: + return "disconnected"; + case SOURCE_STATE_CONNECTING: + return "connecting"; + case SOURCE_STATE_CONNECTED: + return "connected"; + case SOURCE_STATE_PLAYING: + return "playing"; + default: + error("Invalid source state %d", state); + return NULL; + } +} + +static void source_set_state(struct audio_device *dev, source_state_t new_state) +{ + struct source *source = dev->source; + const char *state_str; + source_state_t old_state = source->state; + GSList *l; + + source->state = new_state; + + state_str = state2str(new_state); + if (state_str) + emit_property_changed(dev->conn, dev->path, + AUDIO_SOURCE_INTERFACE, "State", + DBUS_TYPE_STRING, &state_str); + + for (l = source_callbacks; l != NULL; l = l->next) { + struct source_state_callback *cb = l->data; + cb->cb(dev, old_state, new_state, cb->user_data); + } +} + +static void avdtp_state_callback(struct audio_device *dev, + struct avdtp *session, + avdtp_session_state_t old_state, + avdtp_session_state_t new_state, + void *user_data) +{ + struct source *source = dev->source; + + if (source == NULL) + return; + + switch (new_state) { + case AVDTP_SESSION_STATE_DISCONNECTED: + source_set_state(dev, SOURCE_STATE_DISCONNECTED); + break; + case AVDTP_SESSION_STATE_CONNECTING: + source_set_state(dev, SOURCE_STATE_CONNECTING); + break; + case AVDTP_SESSION_STATE_CONNECTED: + break; + } + + source->session_state = new_state; +} + +static void pending_request_free(struct audio_device *dev, + struct pending_request *pending) +{ + if (pending->conn) + dbus_connection_unref(pending->conn); + if (pending->msg) + dbus_message_unref(pending->msg); + if (pending->id) + a2dp_cancel(dev, pending->id); + + g_free(pending); +} + +static void stream_state_changed(struct avdtp_stream *stream, + avdtp_state_t old_state, + avdtp_state_t new_state, + struct avdtp_error *err, + void *user_data) +{ + struct audio_device *dev = user_data; + struct source *source = dev->source; + + if (err) + return; + + switch (new_state) { + case AVDTP_STATE_IDLE: + if (source->disconnect) { + DBusMessage *reply; + struct pending_request *p; + + p = source->disconnect; + source->disconnect = NULL; + + reply = dbus_message_new_method_return(p->msg); + g_dbus_send_message(p->conn, reply); + pending_request_free(dev, p); + } + + if (source->session) { + avdtp_unref(source->session); + source->session = NULL; + } + source->stream = NULL; + source->cb_id = 0; + break; + case AVDTP_STATE_OPEN: + source_set_state(dev, SOURCE_STATE_CONNECTED); + break; + case AVDTP_STATE_STREAMING: + source_set_state(dev, SOURCE_STATE_PLAYING); + break; + case AVDTP_STATE_CONFIGURED: + case AVDTP_STATE_CLOSING: + case AVDTP_STATE_ABORTING: + default: + break; + } + + source->stream_state = new_state; +} + +static void error_failed(DBusConnection *conn, DBusMessage *msg, + const char *desc) +{ + DBusMessage *reply = btd_error_failed(msg, desc); + g_dbus_send_message(conn, reply); +} + +static gboolean stream_setup_retry(gpointer user_data) +{ + struct source *source = user_data; + struct pending_request *pending = source->connect; + + source->retry_id = 0; + + if (source->stream_state >= AVDTP_STATE_OPEN) { + DBG("Stream successfully created, after XCASE connect:connect"); + if (pending->msg) { + DBusMessage *reply; + reply = dbus_message_new_method_return(pending->msg); + g_dbus_send_message(pending->conn, reply); + } + } else { + DBG("Stream setup failed, after XCASE connect:connect"); + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + } + + source->connect = NULL; + pending_request_free(source->dev, pending); + + return FALSE; +} + +static void stream_setup_complete(struct avdtp *session, struct a2dp_sep *sep, + struct avdtp_stream *stream, + struct avdtp_error *err, void *user_data) +{ + struct source *source = user_data; + struct pending_request *pending; + + pending = source->connect; + + pending->id = 0; + + if (stream) { + DBG("Stream successfully created"); + + if (pending->msg) { + DBusMessage *reply; + reply = dbus_message_new_method_return(pending->msg); + g_dbus_send_message(pending->conn, reply); + } + + source->connect = NULL; + pending_request_free(source->dev, pending); + + return; + } + + avdtp_unref(source->session); + source->session = NULL; + if (avdtp_error_category(err) == AVDTP_ERRNO + && avdtp_error_posix_errno(err) != EHOSTDOWN) { + DBG("connect:connect XCASE detected"); + source->retry_id = g_timeout_add_seconds(STREAM_SETUP_RETRY_TIMER, + stream_setup_retry, + source); + } else { + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + source->connect = NULL; + pending_request_free(source->dev, pending); + DBG("Stream setup failed : %s", avdtp_strerror(err)); + } +} + +static void select_complete(struct avdtp *session, struct a2dp_sep *sep, + GSList *caps, void *user_data) +{ + struct source *source = user_data; + struct pending_request *pending; + int id; + + pending = source->connect; + + pending->id = 0; + + if (caps == NULL) + goto failed; + + id = a2dp_config(session, sep, stream_setup_complete, caps, source); + if (id == 0) + goto failed; + + pending->id = id; + return; + +failed: + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + pending_request_free(source->dev, pending); + source->connect = NULL; + avdtp_unref(source->session); + source->session = NULL; +} + +static void discovery_complete(struct avdtp *session, GSList *seps, struct avdtp_error *err, + void *user_data) +{ + struct source *source = user_data; + struct pending_request *pending; + int id; + + pending = source->connect; + + if (err) { + avdtp_unref(source->session); + source->session = NULL; + if (avdtp_error_category(err) == AVDTP_ERRNO + && avdtp_error_posix_errno(err) != EHOSTDOWN) { + DBG("connect:connect XCASE detected"); + source->retry_id = + g_timeout_add_seconds(STREAM_SETUP_RETRY_TIMER, + stream_setup_retry, + source); + } else + goto failed; + return; + } + + DBG("Discovery complete"); + + id = a2dp_select_capabilities(source->session, AVDTP_SEP_TYPE_SOURCE, NULL, + select_complete, source); + if (id == 0) + goto failed; + + pending->id = id; + return; + +failed: + if (pending->msg) + error_failed(pending->conn, pending->msg, "Stream setup failed"); + pending_request_free(source->dev, pending); + source->connect = NULL; + avdtp_unref(source->session); + source->session = NULL; +} + +gboolean source_setup_stream(struct source *source, struct avdtp *session) +{ + if (source->connect || source->disconnect) + return FALSE; + + if (session && !source->session) + source->session = avdtp_ref(session); + + if (!source->session) + return FALSE; + + if (avdtp_discover(source->session, discovery_complete, source) < 0) + return FALSE; + + source->connect = g_new0(struct pending_request, 1); + + return TRUE; +} + +static DBusMessage *source_connect(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *dev = data; + struct source *source = dev->source; + struct pending_request *pending; + + if (!source->session) + source->session = avdtp_get(&dev->src, &dev->dst); + + if (!source->session) + return btd_error_failed(msg, "Unable to get a session"); + + if (source->connect || source->disconnect) + return btd_error_busy(msg); + + if (source->stream_state >= AVDTP_STATE_OPEN) + return btd_error_already_connected(msg); + + if (!source_setup_stream(source, NULL)) + return btd_error_failed(msg, "Failed to create a stream"); + + dev->auto_connect = FALSE; + + pending = source->connect; + + pending->conn = dbus_connection_ref(conn); + pending->msg = dbus_message_ref(msg); + + DBG("stream creation in progress"); + + return NULL; +} + +static DBusMessage *source_disconnect(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + struct source *source = device->source; + struct pending_request *pending; + int err; + + if (!source->session) + return btd_error_not_connected(msg); + + if (source->connect || source->disconnect) + return btd_error_busy(msg); + + if (source->stream_state < AVDTP_STATE_OPEN) { + DBusMessage *reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + avdtp_unref(source->session); + source->session = NULL; + return reply; + } + + err = avdtp_close(source->session, source->stream, FALSE); + if (err < 0) + return btd_error_failed(msg, strerror(-err)); + + pending = g_new0(struct pending_request, 1); + pending->conn = dbus_connection_ref(conn); + pending->msg = dbus_message_ref(msg); + source->disconnect = pending; + + return NULL; +} + +static DBusMessage *source_get_properties(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct audio_device *device = data; + struct source *source = device->source; + DBusMessage *reply; + DBusMessageIter iter; + DBusMessageIter dict; + const char *state; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + /* State */ + state = state2str(source->state); + if (state) + dict_append_entry(&dict, "State", DBUS_TYPE_STRING, &state); + + dbus_message_iter_close_container(&iter, &dict); + + return reply; +} + +static const GDBusMethodTable source_methods[] = { + { GDBUS_ASYNC_METHOD("Connect", NULL, NULL, source_connect) }, + { GDBUS_ASYNC_METHOD("Disconnect", NULL, NULL, source_disconnect) }, + { GDBUS_METHOD("GetProperties", + NULL, GDBUS_ARGS({ "properties", "a{sv}" }), + source_get_properties) }, + { } +}; + +static const GDBusSignalTable source_signals[] = { + { GDBUS_SIGNAL("PropertyChanged", + GDBUS_ARGS({ "name", "s" }, { "value", "v" })) }, + { } +}; + +static void source_free(struct audio_device *dev) +{ + struct source *source = dev->source; + + if (source->cb_id) + avdtp_stream_remove_cb(source->session, source->stream, + source->cb_id); + + if (source->session) + avdtp_unref(source->session); + + if (source->connect) + pending_request_free(dev, source->connect); + + if (source->disconnect) + pending_request_free(dev, source->disconnect); + + if (source->retry_id) + g_source_remove(source->retry_id); + + g_free(source); + dev->source = NULL; +} + +static void path_unregister(void *data) +{ + struct audio_device *dev = data; + + DBG("Unregistered interface %s on path %s", + AUDIO_SOURCE_INTERFACE, dev->path); + + source_free(dev); +} + +void source_unregister(struct audio_device *dev) +{ + g_dbus_unregister_interface(dev->conn, dev->path, + AUDIO_SOURCE_INTERFACE); +} + +struct source *source_init(struct audio_device *dev) +{ + struct source *source; + + if (!g_dbus_register_interface(dev->conn, dev->path, + AUDIO_SOURCE_INTERFACE, + source_methods, source_signals, NULL, + dev, path_unregister)) + return NULL; + + DBG("Registered interface %s on path %s", + AUDIO_SOURCE_INTERFACE, dev->path); + + if (avdtp_callback_id == 0) + avdtp_callback_id = avdtp_add_state_cb(avdtp_state_callback, + NULL); + + source = g_new0(struct source, 1); + + source->dev = dev; + + return source; +} + +gboolean source_is_active(struct audio_device *dev) +{ + struct source *source = dev->source; + + if (source->session) + return TRUE; + + return FALSE; +} + +source_state_t source_get_state(struct audio_device *dev) +{ + struct source *source = dev->source; + + return source->state; +} + +gboolean source_new_stream(struct audio_device *dev, struct avdtp *session, + struct avdtp_stream *stream) +{ + struct source *source = dev->source; + + if (source->stream) + return FALSE; + + if (!source->session) + source->session = avdtp_ref(session); + + source->stream = stream; + + source->cb_id = avdtp_stream_add_cb(session, stream, + stream_state_changed, dev); + + return TRUE; +} + +gboolean source_shutdown(struct source *source) +{ + if (!source->stream) + return FALSE; + + if (avdtp_close(source->session, source->stream, FALSE) < 0) + return FALSE; + + return TRUE; +} + +unsigned int source_add_state_cb(source_state_cb cb, void *user_data) +{ + struct source_state_callback *state_cb; + static unsigned int id = 0; + + state_cb = g_new(struct source_state_callback, 1); + state_cb->cb = cb; + state_cb->user_data = user_data; + state_cb->id = ++id; + + source_callbacks = g_slist_append(source_callbacks, state_cb); + + return state_cb->id; +} + +gboolean source_remove_state_cb(unsigned int id) +{ + GSList *l; + + for (l = source_callbacks; l != NULL; l = l->next) { + struct source_state_callback *cb = l->data; + if (cb && cb->id == id) { + source_callbacks = g_slist_remove(source_callbacks, cb); + g_free(cb); + return TRUE; + } + } + + return FALSE; +} diff --git a/audio/source.h b/audio/source.h new file mode 100644 index 0000000..695bded --- /dev/null +++ b/audio/source.h @@ -0,0 +1,50 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * Copyright (C) 2009 Joao Paulo Rechi Vita + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define AUDIO_SOURCE_INTERFACE "org.bluez.AudioSource" + +typedef enum { + SOURCE_STATE_DISCONNECTED, + SOURCE_STATE_CONNECTING, + SOURCE_STATE_CONNECTED, + SOURCE_STATE_PLAYING, +} source_state_t; + +typedef void (*source_state_cb) (struct audio_device *dev, + source_state_t old_state, + source_state_t new_state, + void *user_data); + +unsigned int source_add_state_cb(source_state_cb cb, void *user_data); +gboolean source_remove_state_cb(unsigned int id); + +struct source *source_init(struct audio_device *dev); +void source_unregister(struct audio_device *dev); +gboolean source_is_active(struct audio_device *dev); +source_state_t source_get_state(struct audio_device *dev); +gboolean source_new_stream(struct audio_device *dev, struct avdtp *session, + struct avdtp_stream *stream); +gboolean source_setup_stream(struct source *source, struct avdtp *session); +gboolean source_shutdown(struct source *source); diff --git a/audio/telephony-dummy.c b/audio/telephony-dummy.c new file mode 100644 index 0000000..2f89139 --- /dev/null +++ b/audio/telephony-dummy.c @@ -0,0 +1,447 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include "log.h" +#include "telephony.h" +#include "error.h" + +#define TELEPHONY_DUMMY_IFACE "org.bluez.TelephonyTest" +#define TELEPHONY_DUMMY_PATH "/org/bluez/test" + +static DBusConnection *connection = NULL; + +static const char *chld_str = "0,1,1x,2,2x,3,4"; +static char *subscriber_number = NULL; +static char *active_call_number = NULL; +static int active_call_status = 0; +static int active_call_dir = 0; + +static gboolean events_enabled = FALSE; + +static struct indicator dummy_indicators[] = +{ + { "battchg", "0-5", 5, TRUE }, + { "signal", "0-5", 5, TRUE }, + { "service", "0,1", 1, TRUE }, + { "call", "0,1", 0, TRUE }, + { "callsetup", "0-3", 0, TRUE }, + { "callheld", "0-2", 0, FALSE }, + { "roam", "0,1", 0, TRUE }, + { NULL } +}; + +void telephony_device_connected(void *telephony_device) +{ + DBG("telephony-dummy: device %p connected", telephony_device); +} + +void telephony_device_disconnected(void *telephony_device) +{ + DBG("telephony-dummy: device %p disconnected", telephony_device); + events_enabled = FALSE; +} + +void telephony_event_reporting_req(void *telephony_device, int ind) +{ + events_enabled = ind == 1 ? TRUE : FALSE; + + telephony_event_reporting_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_response_and_hold_req(void *telephony_device, int rh) +{ + telephony_response_and_hold_rsp(telephony_device, + CME_ERROR_NOT_SUPPORTED); +} + +void telephony_last_dialed_number_req(void *telephony_device) +{ + telephony_last_dialed_number_rsp(telephony_device, CME_ERROR_NONE); + + /* Notify outgoing call set-up successfully initiated */ + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + + active_call_status = CALL_STATUS_ALERTING; + active_call_dir = CALL_DIR_OUTGOING; +} + +void telephony_terminate_call_req(void *telephony_device) +{ + g_free(active_call_number); + active_call_number = NULL; + + telephony_terminate_call_rsp(telephony_device, CME_ERROR_NONE); + + if (telephony_get_indicator(dummy_indicators, "callsetup") > 0) + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_INACTIVE); + else + telephony_update_indicator(dummy_indicators, "call", + EV_CALL_INACTIVE); +} + +void telephony_answer_call_req(void *telephony_device) +{ + telephony_answer_call_rsp(telephony_device, CME_ERROR_NONE); + + telephony_update_indicator(dummy_indicators, "call", EV_CALL_ACTIVE); + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_INACTIVE); + + active_call_status = CALL_STATUS_ACTIVE; +} + +void telephony_dial_number_req(void *telephony_device, const char *number) +{ + g_free(active_call_number); + active_call_number = g_strdup(number); + + DBG("telephony-dummy: dial request to %s", active_call_number); + + telephony_dial_number_rsp(telephony_device, CME_ERROR_NONE); + + /* Notify outgoing call set-up successfully initiated */ + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + + active_call_status = CALL_STATUS_ALERTING; + active_call_dir = CALL_DIR_OUTGOING; +} + +void telephony_transmit_dtmf_req(void *telephony_device, char tone) +{ + DBG("telephony-dummy: transmit dtmf: %c", tone); + telephony_transmit_dtmf_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_subscriber_number_req(void *telephony_device) +{ + DBG("telephony-dummy: subscriber number request"); + if (subscriber_number) + telephony_subscriber_number_ind(subscriber_number, + NUMBER_TYPE_TELEPHONY, + SUBSCRIBER_SERVICE_VOICE); + telephony_subscriber_number_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_list_current_calls_req(void *telephony_device) +{ + DBG("telephony-dummy: list current calls request"); + if (active_call_number) + telephony_list_current_call_ind(1, active_call_dir, + active_call_status, + CALL_MODE_VOICE, + CALL_MULTIPARTY_NO, + active_call_number, + NUMBER_TYPE_TELEPHONY); + telephony_list_current_calls_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_operator_selection_req(void *telephony_device) +{ + telephony_operator_selection_ind(OPERATOR_MODE_AUTO, "DummyOperator"); + telephony_operator_selection_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_call_hold_req(void *telephony_device, const char *cmd) +{ + DBG("telephony-dymmy: got call hold request %s", cmd); + telephony_call_hold_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_nr_and_ec_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-dummy: got %s NR and EC request", + enable ? "enable" : "disable"); + + telephony_nr_and_ec_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_voice_dial_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-dummy: got %s voice dial request", + enable ? "enable" : "disable"); + + g_dbus_emit_signal(connection, TELEPHONY_DUMMY_PATH, + TELEPHONY_DUMMY_IFACE, "VoiceDial", + DBUS_TYPE_INVALID); + + telephony_voice_dial_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_key_press_req(void *telephony_device, const char *keys) +{ + DBG("telephony-dummy: got key press request for %s", keys); + telephony_key_press_rsp(telephony_device, CME_ERROR_NONE); +} + +/* D-Bus method handlers */ +static DBusMessage *outgoing_call(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + const char *number; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + DBG("telephony-dummy: outgoing call to %s", number); + + g_free(active_call_number); + active_call_number = g_strdup(number); + + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + + active_call_status = CALL_STATUS_ALERTING; + active_call_dir = CALL_DIR_OUTGOING; + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *incoming_call(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + const char *number; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + DBG("telephony-dummy: incoming call to %s", number); + + g_free(active_call_number); + active_call_number = g_strdup(number); + + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_INCOMING); + + active_call_status = CALL_STATUS_INCOMING; + active_call_dir = CALL_DIR_INCOMING; + + telephony_incoming_call_ind(number, NUMBER_TYPE_TELEPHONY); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *cancel_call(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + DBG("telephony-dummy: cancel call"); + + g_free(active_call_number); + active_call_number = NULL; + + if (telephony_get_indicator(dummy_indicators, "callsetup") > 0) { + telephony_update_indicator(dummy_indicators, "callsetup", + EV_CALLSETUP_INACTIVE); + telephony_calling_stopped_ind(); + } + + if (telephony_get_indicator(dummy_indicators, "call") > 0) + telephony_update_indicator(dummy_indicators, "call", + EV_CALL_INACTIVE); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *signal_strength(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + dbus_uint32_t strength; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_UINT32, &strength, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + if (strength > 5) + return btd_error_invalid_args(msg); + + telephony_update_indicator(dummy_indicators, "signal", strength); + + DBG("telephony-dummy: signal strength set to %u", strength); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *battery_level(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + dbus_uint32_t level; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_UINT32, &level, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + if (level > 5) + return btd_error_invalid_args(msg); + + telephony_update_indicator(dummy_indicators, "battchg", level); + + DBG("telephony-dummy: battery level set to %u", level); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *roaming_status(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + dbus_bool_t roaming; + int val; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_BOOLEAN, &roaming, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + val = roaming ? EV_ROAM_ACTIVE : EV_ROAM_INACTIVE; + + telephony_update_indicator(dummy_indicators, "roam", val); + + DBG("telephony-dummy: roaming status set to %d", val); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *registration_status(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + dbus_bool_t registration; + int val; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_BOOLEAN, ®istration, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + val = registration ? EV_SERVICE_PRESENT : EV_SERVICE_NONE; + + telephony_update_indicator(dummy_indicators, "service", val); + + DBG("telephony-dummy: registration status set to %d", val); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *set_subscriber_number(DBusConnection *conn, + DBusMessage *msg, + void *data) +{ + const char *number; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) + return btd_error_invalid_args(msg); + + g_free(subscriber_number); + subscriber_number = g_strdup(number); + + DBG("telephony-dummy: subscriber number set to %s", number); + + return dbus_message_new_method_return(msg); +} + +static const GDBusMethodTable dummy_methods[] = { + { GDBUS_METHOD("OutgoingCall", + GDBUS_ARGS({ "number", "s" }), NULL, + outgoing_call) }, + { GDBUS_METHOD("IncomingCall", + GDBUS_ARGS({ "number", "s" }), NULL, + incoming_call) }, + { GDBUS_METHOD("CancelCall", NULL, NULL, cancel_call) }, + { GDBUS_METHOD("SignalStrength", + GDBUS_ARGS({ "strength", "u" }), NULL, + signal_strength) }, + { GDBUS_METHOD("BatteryLevel", + GDBUS_ARGS({ "level", "u" }), NULL, + battery_level) }, + { GDBUS_METHOD("RoamingStatus", + GDBUS_ARGS({ "roaming", "b" }), NULL, + roaming_status) }, + { GDBUS_METHOD("RegistrationStatus", + GDBUS_ARGS({ "registration", "b" }), NULL, + registration_status) }, + { GDBUS_METHOD("SetSubscriberNumber", + GDBUS_ARGS({ "number", "s" }), NULL, + set_subscriber_number) }, + { } +}; + +static const GDBusSignalTable dummy_signals[] = { + { GDBUS_SIGNAL("VoiceDial", NULL) }, + { } +}; + +int telephony_init(void) +{ + uint32_t features = AG_FEATURE_REJECT_A_CALL | + AG_FEATURE_ENHANCED_CALL_STATUS | + AG_FEATURE_EXTENDED_ERROR_RESULT_CODES; + + DBG(""); + + connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + + if (g_dbus_register_interface(connection, TELEPHONY_DUMMY_PATH, + TELEPHONY_DUMMY_IFACE, + dummy_methods, dummy_signals, + NULL, NULL, NULL) == FALSE) { + error("telephony-dummy interface %s init failed on path %s", + TELEPHONY_DUMMY_IFACE, TELEPHONY_DUMMY_PATH); + return -1; + } + + telephony_ready_ind(features, dummy_indicators, BTRH_NOT_SUPPORTED, + chld_str); + + return 0; +} + +void telephony_exit(void) +{ + DBG(""); + + g_dbus_unregister_interface(connection, TELEPHONY_DUMMY_PATH, + TELEPHONY_DUMMY_IFACE); + dbus_connection_unref(connection); + connection = NULL; + + telephony_deinit(); +} diff --git a/audio/telephony-maemo5.c b/audio/telephony-maemo5.c new file mode 100644 index 0000000..8a00296 --- /dev/null +++ b/audio/telephony-maemo5.c @@ -0,0 +1,2105 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2008-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "log.h" +#include "telephony.h" +#include "error.h" + +/* SSC D-Bus definitions */ +#define SSC_DBUS_NAME "com.nokia.phone.SSC" +#define SSC_DBUS_IFACE "com.nokia.phone.SSC" +#define SSC_DBUS_PATH "/com/nokia/phone/SSC" + +/* libcsnet D-Bus definitions */ +#define NETWORK_BUS_NAME "com.nokia.phone.net" +#define NETWORK_INTERFACE "Phone.Net" +#define NETWORK_PATH "/com/nokia/phone/net" + +/* Mask bits for supported services */ +#define NETWORK_MASK_GPRS_SUPPORT 0x01 +#define NETWORK_MASK_CS_SERVICES 0x02 +#define NETWORK_MASK_EGPRS_SUPPORT 0x04 +#define NETWORK_MASK_HSDPA_AVAIL 0x08 +#define NETWORK_MASK_HSUPA_AVAIL 0x10 + +/* network get cell info: cell type */ +#define NETWORK_UNKNOWN_CELL 0 +#define NETWORK_GSM_CELL 1 +#define NETWORK_WCDMA_CELL 2 + +enum net_registration_status { + NETWORK_REG_STATUS_HOME = 0x00, + NETWORK_REG_STATUS_ROAM, + NETWORK_REG_STATUS_ROAM_BLINK, + NETWORK_REG_STATUS_NOSERV, + NETWORK_REG_STATUS_NOSERV_SEARCHING, + NETWORK_REG_STATUS_NOSERV_NOTSEARCHING, + NETWORK_REG_STATUS_NOSERV_NOSIM, + NETWORK_REG_STATUS_POWER_OFF = 0x08, + NETWORK_REG_STATUS_NSPS, + NETWORK_REG_STATUS_NSPS_NO_COVERAGE, + NETWORK_REG_STATUS_NOSERV_SIM_REJECTED_BY_NW +}; + +enum network_types { + NETWORK_GSM_HOME_PLMN = 0, + NETWORK_GSM_PREFERRED_PLMN, + NETWORK_GSM_FORBIDDEN_PLMN, + NETWORK_GSM_OTHER_PLMN, + NETWORK_GSM_NO_PLMN_AVAIL +}; + +enum network_alpha_tag_name_type { + NETWORK_HARDCODED_LATIN_OPER_NAME = 0, + NETWORK_HARDCODED_USC2_OPER_NAME, + NETWORK_NITZ_SHORT_OPER_NAME, + NETWORK_NITZ_FULL_OPER_NAME, +}; + +#define TELEPHONY_MAEMO_PATH "/com/nokia/MaemoTelephony" +#define TELEPHONY_MAEMO_INTERFACE "com.nokia.MaemoTelephony" + +#define CALLERID_BASE "/var/lib/bluetooth/maemo-callerid-" +#define ALLOWED_FLAG_FILE "/var/lib/bluetooth/maemo-callerid-allowed" +#define RESTRICTED_FLAG_FILE "/var/lib/bluetooth/maemo-callerid-restricted" +#define NONE_FLAG_FILE "/var/lib/bluetooth/maemo-callerid-none" + +static uint32_t callerid = 0; + +/* CSD CALL plugin D-Bus definitions */ +#define CSD_CALL_BUS_NAME "com.nokia.csd.Call" +#define CSD_CALL_INTERFACE "com.nokia.csd.Call" +#define CSD_CALL_INSTANCE "com.nokia.csd.Call.Instance" +#define CSD_CALL_CONFERENCE "com.nokia.csd.Call.Conference" +#define CSD_CALL_PATH "/com/nokia/csd/call" +#define CSD_CALL_CONFERENCE_PATH "/com/nokia/csd/call/conference" + +/* Call status values as exported by the CSD CALL plugin */ +#define CSD_CALL_STATUS_IDLE 0 +#define CSD_CALL_STATUS_CREATE 1 +#define CSD_CALL_STATUS_COMING 2 +#define CSD_CALL_STATUS_PROCEEDING 3 +#define CSD_CALL_STATUS_MO_ALERTING 4 +#define CSD_CALL_STATUS_MT_ALERTING 5 +#define CSD_CALL_STATUS_WAITING 6 +#define CSD_CALL_STATUS_ANSWERED 7 +#define CSD_CALL_STATUS_ACTIVE 8 +#define CSD_CALL_STATUS_MO_RELEASE 9 +#define CSD_CALL_STATUS_MT_RELEASE 10 +#define CSD_CALL_STATUS_HOLD_INITIATED 11 +#define CSD_CALL_STATUS_HOLD 12 +#define CSD_CALL_STATUS_RETRIEVE_INITIATED 13 +#define CSD_CALL_STATUS_RECONNECT_PENDING 14 +#define CSD_CALL_STATUS_TERMINATED 15 +#define CSD_CALL_STATUS_SWAP_INITIATED 16 + +#define CALL_FLAG_NONE 0 +#define CALL_FLAG_PRESENTATION_ALLOWED 0x01 +#define CALL_FLAG_PRESENTATION_RESTRICTED 0x02 + +/* SIM Phonebook D-Bus definitions */ +#define SIM_PHONEBOOK_BUS_NAME "com.nokia.phone.SIM" +#define SIM_PHONEBOOK_INTERFACE "Phone.Sim.Phonebook" +#define SIM_PHONEBOOK_PATH "/com/nokia/phone/SIM/phonebook" + +#define PHONEBOOK_INDEX_FIRST_ENTRY 0xFFFF +#define PHONEBOOK_INDEX_NEXT_FREE_LOCATION 0xFFFE + +enum sim_phonebook_type { + SIM_PHONEBOOK_TYPE_ADN = 0x0, + SIM_PHONEBOOK_TYPE_SDN, + SIM_PHONEBOOK_TYPE_FDN, + SIM_PHONEBOOK_TYPE_VMBX, + SIM_PHONEBOOK_TYPE_MBDN, + SIM_PHONEBOOK_TYPE_EN, + SIM_PHONEBOOK_TYPE_MSISDN +}; + +enum sim_phonebook_location_type { + SIM_PHONEBOOK_LOCATION_EXACT = 0x0, + SIM_PHONEBOOK_LOCATION_NEXT +}; + +struct csd_call { + char *object_path; + int status; + gboolean originating; + gboolean emergency; + gboolean on_hold; + gboolean conference; + char *number; + gboolean setup; +}; + +static struct { + uint8_t status; + uint16_t lac; + uint32_t cell_id; + uint32_t operator_code; + uint32_t country_code; + uint8_t network_type; + uint8_t supported_services; + uint16_t signals_bar; + char *operator_name; +} net = { + .status = NETWORK_REG_STATUS_NOSERV, + .lac = 0, + .cell_id = 0, + .operator_code = 0, + .country_code = 0, + .network_type = NETWORK_GSM_NO_PLMN_AVAIL, + .supported_services = 0, + .signals_bar = 0, + .operator_name = NULL, +}; + +static DBusConnection *connection = NULL; + +static GSList *calls = NULL; + +/* Reference count for determining the call indicator status */ +static GSList *active_calls = NULL; + +static char *msisdn = NULL; /* Subscriber number */ +static char *vmbx = NULL; /* Voice mailbox number */ + +/* HAL battery namespace key values */ +static int battchg_cur = -1; /* "battery.charge_level.current" */ +static int battchg_last = -1; /* "battery.charge_level.last_full" */ +static int battchg_design = -1; /* "battery.charge_level.design" */ + +static gboolean get_calls_active = FALSE; + +static gboolean events_enabled = FALSE; + +/* Supported set of call hold operations */ +static const char *chld_str = "0,1,1x,2,2x,3,4"; + +static char *last_dialed_number = NULL; + +/* Timer for tracking call creation requests */ +static guint create_request_timer = 0; + +static struct indicator maemo_indicators[] = +{ + { "battchg", "0-5", 5, TRUE }, + { "signal", "0-5", 0, TRUE }, + { "service", "0,1", 0, TRUE }, + { "call", "0,1", 0, TRUE }, + { "callsetup", "0-3", 0, TRUE }, + { "callheld", "0-2", 0, FALSE }, + { "roam", "0,1", 0, TRUE }, + { NULL } +}; + +static char *call_status_str[] = { + "IDLE", + "CREATE", + "COMING", + "PROCEEDING", + "MO_ALERTING", + "MT_ALERTING", + "WAITING", + "ANSWERED", + "ACTIVE", + "MO_RELEASE", + "MT_RELEASE", + "HOLD_INITIATED", + "HOLD", + "RETRIEVE_INITIATED", + "RECONNECT_PENDING", + "TERMINATED", + "SWAP_INITIATED", + "???" +}; + +static struct csd_call *find_call(const char *path) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (g_str_equal(call->object_path, path)) + return call; + } + + return NULL; +} + +static struct csd_call *find_non_held_call(void) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == CSD_CALL_STATUS_IDLE) + continue; + + if (call->status != CSD_CALL_STATUS_HOLD) + return call; + } + + return NULL; +} + +static struct csd_call *find_non_idle_call(void) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status != CSD_CALL_STATUS_IDLE) + return call; + } + + return NULL; +} + +static struct csd_call *find_call_with_status(int status) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == status) + return call; + } + + return NULL; +} + +static int release_conference(void) +{ + DBusMessage *msg; + + DBG("telephony-maemo: releasing conference call"); + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + CSD_CALL_CONFERENCE_PATH, + CSD_CALL_INSTANCE, + "Release"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int release_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + call->object_path, + CSD_CALL_INSTANCE, + "Release"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int answer_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + call->object_path, + CSD_CALL_INSTANCE, + "Answer"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int split_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + call->object_path, + CSD_CALL_INSTANCE, + "Split"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int unhold_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Unhold"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int hold_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Hold"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int swap_calls(void) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Swap"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int create_conference(void) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Conference"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int call_transfer(void) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Transfer"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int number_type(const char *number) +{ + if (number == NULL) + return NUMBER_TYPE_TELEPHONY; + + if (number[0] == '+' || strncmp(number, "00", 2) == 0) + return NUMBER_TYPE_INTERNATIONAL; + + return NUMBER_TYPE_TELEPHONY; +} + +void telephony_device_connected(void *telephony_device) +{ + struct csd_call *coming; + + DBG("telephony-maemo: device %p connected", telephony_device); + + coming = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + if (coming) { + if (find_call_with_status(CSD_CALL_STATUS_ACTIVE)) + telephony_call_waiting_ind(coming->number, + number_type(coming->number)); + else + telephony_incoming_call_ind(coming->number, + number_type(coming->number)); + } +} + +void telephony_device_disconnected(void *telephony_device) +{ + DBG("telephony-maemo: device %p disconnected", telephony_device); + events_enabled = FALSE; +} + +void telephony_event_reporting_req(void *telephony_device, int ind) +{ + events_enabled = ind == 1 ? TRUE : FALSE; + + telephony_event_reporting_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_response_and_hold_req(void *telephony_device, int rh) +{ + telephony_response_and_hold_rsp(telephony_device, + CME_ERROR_NOT_SUPPORTED); +} + +void telephony_last_dialed_number_req(void *telephony_device) +{ + DBG("telephony-maemo: last dialed number request"); + + if (last_dialed_number) + telephony_dial_number_req(telephony_device, + last_dialed_number); + else + telephony_last_dialed_number_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); +} + +void telephony_terminate_call_req(void *telephony_device) +{ + struct csd_call *call; + int err; + + call = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + if (!call) + call = find_non_idle_call(); + + if (!call) { + error("No active call"); + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + if (call->conference) + err = release_conference(); + else + err = release_call(call); + + if (err < 0) + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_terminate_call_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_answer_call_req(void *telephony_device) +{ + struct csd_call *call; + + call = find_call_with_status(CSD_CALL_STATUS_COMING); + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_PROCEEDING); + + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_WAITING); + + if (!call) { + telephony_answer_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + if (answer_call(call) < 0) + telephony_answer_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_answer_call_rsp(telephony_device, CME_ERROR_NONE); +} + +static int send_method_call(const char *dest, const char *path, + const char *interface, const char *method, + DBusPendingCallNotifyFunction cb, + void *user_data, int type, ...) +{ + DBusMessage *msg; + DBusPendingCall *call; + va_list args; + + msg = dbus_message_new_method_call(dest, path, interface, method); + if (!msg) { + error("Unable to allocate new D-Bus %s message", method); + return -ENOMEM; + } + + va_start(args, type); + + if (!dbus_message_append_args_valist(msg, type, args)) { + dbus_message_unref(msg); + va_end(args); + return -EIO; + } + + va_end(args); + + if (!cb) { + g_dbus_send_message(connection, msg); + return 0; + } + + if (!dbus_connection_send_with_reply(connection, msg, &call, -1)) { + error("Sending %s failed", method); + dbus_message_unref(msg); + return -EIO; + } + + dbus_pending_call_set_notify(call, cb, user_data, NULL); + dbus_pending_call_unref(call); + dbus_message_unref(msg); + + return 0; +} + +static const char *memory_dial_lookup(int location) +{ + if (location == 1) + return vmbx; + else + return NULL; +} + +void telephony_dial_number_req(void *telephony_device, const char *number) +{ + uint32_t flags = callerid; + int ret; + + DBG("telephony-maemo: dial request to %s", number); + + if (strncmp(number, "*31#", 4) == 0) { + number += 4; + flags = CALL_FLAG_PRESENTATION_ALLOWED; + } else if (strncmp(number, "#31#", 4) == 0) { + number += 4; + flags = CALL_FLAG_PRESENTATION_RESTRICTED; + } else if (number[0] == '>') { + const char *location = &number[1]; + + number = memory_dial_lookup(strtol(&number[1], NULL, 0)); + if (!number) { + error("No number at memory location %s", location); + telephony_dial_number_rsp(telephony_device, + CME_ERROR_INVALID_INDEX); + return; + } + } + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "CreateWith", + NULL, NULL, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_UINT32, &flags, + DBUS_TYPE_INVALID); + if (ret < 0) { + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_dial_number_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_transmit_dtmf_req(void *telephony_device, char tone) +{ + int ret; + char buf[2] = { tone, '\0' }, *buf_ptr = buf; + + DBG("telephony-maemo: transmit dtmf: %s", buf); + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "SendDTMF", + NULL, NULL, + DBUS_TYPE_STRING, &buf_ptr, + DBUS_TYPE_INVALID); + if (ret < 0) { + telephony_transmit_dtmf_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_transmit_dtmf_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_subscriber_number_req(void *telephony_device) +{ + DBG("telephony-maemo: subscriber number request"); + if (msisdn) + telephony_subscriber_number_ind(msisdn, + number_type(msisdn), + SUBSCRIBER_SERVICE_VOICE); + telephony_subscriber_number_rsp(telephony_device, CME_ERROR_NONE); +} + +static int csd_status_to_hfp(struct csd_call *call) +{ + switch (call->status) { + case CSD_CALL_STATUS_IDLE: + case CSD_CALL_STATUS_MO_RELEASE: + case CSD_CALL_STATUS_MT_RELEASE: + case CSD_CALL_STATUS_TERMINATED: + return -1; + case CSD_CALL_STATUS_CREATE: + return CALL_STATUS_DIALING; + case CSD_CALL_STATUS_WAITING: + return CALL_STATUS_WAITING; + case CSD_CALL_STATUS_PROCEEDING: + /* PROCEEDING can happen in outgoing/incoming */ + if (call->originating) + return CALL_STATUS_DIALING; + else + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_COMING: + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_MO_ALERTING: + return CALL_STATUS_ALERTING; + case CSD_CALL_STATUS_MT_ALERTING: + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_ANSWERED: + case CSD_CALL_STATUS_ACTIVE: + case CSD_CALL_STATUS_RECONNECT_PENDING: + case CSD_CALL_STATUS_SWAP_INITIATED: + case CSD_CALL_STATUS_HOLD_INITIATED: + return CALL_STATUS_ACTIVE; + case CSD_CALL_STATUS_RETRIEVE_INITIATED: + case CSD_CALL_STATUS_HOLD: + return CALL_STATUS_HELD; + default: + return -1; + } +} + +void telephony_list_current_calls_req(void *telephony_device) +{ + GSList *l; + int i; + + DBG("telephony-maemo: list current calls request"); + + for (l = calls, i = 1; l != NULL; l = l->next, i++) { + struct csd_call *call = l->data; + int status, direction, multiparty; + + status = csd_status_to_hfp(call); + if (status < 0) + continue; + + direction = call->originating ? + CALL_DIR_OUTGOING : CALL_DIR_INCOMING; + + multiparty = call->conference ? + CALL_MULTIPARTY_YES : CALL_MULTIPARTY_NO; + + telephony_list_current_call_ind(i, direction, status, + CALL_MODE_VOICE, multiparty, + call->number, + number_type(call->number)); + } + + telephony_list_current_calls_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_operator_selection_req(void *telephony_device) +{ + telephony_operator_selection_ind(OPERATOR_MODE_AUTO, + net.operator_name ? net.operator_name : ""); + telephony_operator_selection_rsp(telephony_device, CME_ERROR_NONE); +} + +static void foreach_call_with_status(int status, + int (*func)(struct csd_call *call)) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == status) + func(call); + } +} + +void telephony_call_hold_req(void *telephony_device, const char *cmd) +{ + const char *idx; + struct csd_call *call; + int err = 0; + + DBG("telephony-maemo: got call hold request %s", cmd); + + if (strlen(cmd) > 1) + idx = &cmd[1]; + else + idx = NULL; + + if (idx) + call = g_slist_nth_data(calls, strtol(idx, NULL, 0) - 1); + else + call = NULL; + + switch (cmd[0]) { + case '0': + foreach_call_with_status(CSD_CALL_STATUS_HOLD, release_call); + foreach_call_with_status(CSD_CALL_STATUS_WAITING, + release_call); + break; + case '1': + if (idx) { + if (call) + err = release_call(call); + break; + } + foreach_call_with_status(CSD_CALL_STATUS_ACTIVE, release_call); + call = find_call_with_status(CSD_CALL_STATUS_WAITING); + if (call) + err = answer_call(call); + break; + case '2': + if (idx) { + if (call) + err = split_call(call); + } else { + struct csd_call *held, *wait; + + call = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + held = find_call_with_status(CSD_CALL_STATUS_HOLD); + wait = find_call_with_status(CSD_CALL_STATUS_WAITING); + + if (wait) + err = answer_call(wait); + else if (call && held) + err = swap_calls(); + else { + if (call) + err = hold_call(call); + if (held) + err = unhold_call(held); + } + } + break; + case '3': + if (find_call_with_status(CSD_CALL_STATUS_HOLD) || + find_call_with_status(CSD_CALL_STATUS_WAITING)) + err = create_conference(); + break; + case '4': + err = call_transfer(); + break; + default: + DBG("Unknown call hold request"); + break; + } + + if (err) + telephony_call_hold_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_call_hold_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_nr_and_ec_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-maemo: got %s NR and EC request", + enable ? "enable" : "disable"); + telephony_nr_and_ec_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_key_press_req(void *telephony_device, const char *keys) +{ + struct csd_call *active, *waiting; + int err; + + DBG("telephony-maemo: got key press request for %s", keys); + + waiting = find_call_with_status(CSD_CALL_STATUS_COMING); + if (!waiting) + waiting = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + if (!waiting) + waiting = find_call_with_status(CSD_CALL_STATUS_PROCEEDING); + + active = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + + if (waiting) + err = answer_call(waiting); + else if (active) + err = release_call(active); + else + err = 0; + + if (err < 0) + telephony_key_press_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_key_press_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_voice_dial_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-maemo: got %s voice dial request", + enable ? "enable" : "disable"); + + telephony_voice_dial_rsp(telephony_device, CME_ERROR_NOT_SUPPORTED); +} + +static void handle_incoming_call(DBusMessage *msg) +{ + const char *number, *call_path; + struct csd_call *call; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &call_path, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Call.Coming() signal"); + return; + } + + call = find_call(call_path); + if (!call) { + error("Didn't find any matching call object for %s", + call_path); + return; + } + + DBG("Incoming call to %s from number %s", call_path, number); + + g_free(call->number); + call->number = g_strdup(number); + + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_INCOMING); + + if (find_call_with_status(CSD_CALL_STATUS_ACTIVE)) + telephony_call_waiting_ind(call->number, + number_type(call->number)); + else + telephony_incoming_call_ind(call->number, + number_type(call->number)); +} + +static void handle_outgoing_call(DBusMessage *msg) +{ + const char *number, *call_path; + struct csd_call *call; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &call_path, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Call.Created() signal"); + return; + } + + call = find_call(call_path); + if (!call) { + error("Didn't find any matching call object for %s", + call_path); + return; + } + + DBG("Outgoing call from %s to number %s", call_path, number); + + g_free(call->number); + call->number = g_strdup(number); + + g_free(last_dialed_number); + last_dialed_number = g_strdup(number); + + if (create_request_timer) { + g_source_remove(create_request_timer); + create_request_timer = 0; + } +} + +static gboolean create_timeout(gpointer user_data) +{ + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_INACTIVE); + create_request_timer = 0; + return FALSE; +} + +static void handle_create_requested(DBusMessage *msg) +{ + DBG("Call.CreateRequested()"); + + if (create_request_timer) + g_source_remove(create_request_timer); + + create_request_timer = g_timeout_add_seconds(5, create_timeout, NULL); + + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); +} + +static void handle_call_status(DBusMessage *msg, const char *call_path) +{ + struct csd_call *call; + dbus_uint32_t status, cause_type, cause; + int callheld = telephony_get_indicator(maemo_indicators, "callheld"); + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_UINT32, &status, + DBUS_TYPE_UINT32, &cause_type, + DBUS_TYPE_UINT32, &cause, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Instance.CallStatus() signal"); + return; + } + + call = find_call(call_path); + if (!call) { + error("Didn't find any matching call object for %s", + call_path); + return; + } + + if (status > 16) { + error("Invalid call status %u", status); + return; + } + + DBG("Call %s changed from %s to %s", call_path, + call_status_str[call->status], call_status_str[status]); + + if (call->status == (int) status) { + DBG("Ignoring CSD Call state change to existing state"); + return; + } + + call->status = (int) status; + + switch (status) { + case CSD_CALL_STATUS_IDLE: + if (call->setup) { + telephony_update_indicator(maemo_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + if (!call->originating) + telephony_calling_stopped_ind(); + } + + g_free(call->number); + call->number = NULL; + call->originating = FALSE; + call->emergency = FALSE; + call->on_hold = FALSE; + call->conference = FALSE; + call->setup = FALSE; + break; + case CSD_CALL_STATUS_CREATE: + call->originating = TRUE; + call->setup = TRUE; + break; + case CSD_CALL_STATUS_COMING: + call->originating = FALSE; + call->setup = TRUE; + break; + case CSD_CALL_STATUS_PROCEEDING: + break; + case CSD_CALL_STATUS_MO_ALERTING: + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + break; + case CSD_CALL_STATUS_MT_ALERTING: + break; + case CSD_CALL_STATUS_WAITING: + break; + case CSD_CALL_STATUS_ANSWERED: + break; + case CSD_CALL_STATUS_ACTIVE: + if (call->on_hold) { + call->on_hold = FALSE; + if (find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + else + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_NONE); + } else { + if (!g_slist_find(active_calls, call)) + active_calls = g_slist_prepend(active_calls, call); + if (g_slist_length(active_calls) == 1) + telephony_update_indicator(maemo_indicators, + "call", + EV_CALL_ACTIVE); + /* Upgrade callheld status if necessary */ + if (callheld == EV_CALLHELD_ON_HOLD) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + telephony_update_indicator(maemo_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + if (!call->originating) + telephony_calling_stopped_ind(); + call->setup = FALSE; + } + break; + case CSD_CALL_STATUS_MO_RELEASE: + case CSD_CALL_STATUS_MT_RELEASE: + active_calls = g_slist_remove(active_calls, call); + if (g_slist_length(active_calls) == 0) + telephony_update_indicator(maemo_indicators, "call", + EV_CALL_INACTIVE); + break; + case CSD_CALL_STATUS_HOLD_INITIATED: + break; + case CSD_CALL_STATUS_HOLD: + call->on_hold = TRUE; + if (find_non_held_call()) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + else + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_ON_HOLD); + break; + case CSD_CALL_STATUS_RETRIEVE_INITIATED: + break; + case CSD_CALL_STATUS_RECONNECT_PENDING: + break; + case CSD_CALL_STATUS_TERMINATED: + if (call->on_hold && + !find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_NONE); + else if (callheld == EV_CALLHELD_MULTIPLE && + find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_ON_HOLD); + break; + case CSD_CALL_STATUS_SWAP_INITIATED: + break; + default: + error("Unknown call status %u", status); + break; + } +} + +static void handle_conference(DBusMessage *msg, gboolean joined) +{ + const char *path; + struct csd_call *call; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Conference.%s", + dbus_message_get_member(msg)); + return; + } + + call = find_call(path); + if (!call) { + error("Conference signal for unknown call %s", path); + return; + } + + DBG("Call %s %s the conference", path, joined ? "joined" : "left"); + + call->conference = joined; +} + +static void get_operator_name_reply(DBusPendingCall *pending_call, + void *user_data) +{ + DBusMessage *reply; + DBusError err; + const char *name; + dbus_int32_t net_err; + + reply = dbus_pending_call_steal_reply(pending_call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("get_operator_name failed: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_error_init(&err); + if (!dbus_message_get_args(reply, &err, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INT32, &net_err, + DBUS_TYPE_INVALID)) { + error("Unexpected get_operator_name reply parameters: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + if (net_err != 0) { + error("get_operator_name failed with code %d", net_err); + goto done; + } + + if (strlen(name) == 0) + goto done; + + g_free(net.operator_name); + net.operator_name = g_strdup(name); + + DBG("telephony-maemo: operator name updated: %s", name); + +done: + dbus_message_unref(reply); +} + +static void resolve_operator_name(uint32_t operator, uint32_t country) +{ + uint8_t name_type = NETWORK_HARDCODED_LATIN_OPER_NAME; + + send_method_call(NETWORK_BUS_NAME, NETWORK_PATH, + NETWORK_INTERFACE, "get_operator_name", + get_operator_name_reply, NULL, + DBUS_TYPE_BYTE, &name_type, + DBUS_TYPE_UINT32, &operator, + DBUS_TYPE_UINT32, &country, + DBUS_TYPE_INVALID); +} + +static void update_registration_status(uint8_t status, uint16_t lac, + uint32_t cell_id, + uint32_t operator_code, + uint32_t country_code, + uint8_t network_type, + uint8_t supported_services) +{ + if (net.status != status) { + switch (status) { + case NETWORK_REG_STATUS_HOME: + telephony_update_indicator(maemo_indicators, "roam", + EV_ROAM_INACTIVE); + if (net.status >= NETWORK_REG_STATUS_NOSERV) + telephony_update_indicator(maemo_indicators, + "service", + EV_SERVICE_PRESENT); + break; + case NETWORK_REG_STATUS_ROAM: + case NETWORK_REG_STATUS_ROAM_BLINK: + telephony_update_indicator(maemo_indicators, "roam", + EV_ROAM_ACTIVE); + if (net.status >= NETWORK_REG_STATUS_NOSERV) + telephony_update_indicator(maemo_indicators, + "service", + EV_SERVICE_PRESENT); + break; + case NETWORK_REG_STATUS_NOSERV: + case NETWORK_REG_STATUS_NOSERV_SEARCHING: + case NETWORK_REG_STATUS_NOSERV_NOTSEARCHING: + case NETWORK_REG_STATUS_NOSERV_NOSIM: + case NETWORK_REG_STATUS_POWER_OFF: + case NETWORK_REG_STATUS_NSPS: + case NETWORK_REG_STATUS_NSPS_NO_COVERAGE: + case NETWORK_REG_STATUS_NOSERV_SIM_REJECTED_BY_NW: + if (net.status < NETWORK_REG_STATUS_NOSERV) + telephony_update_indicator(maemo_indicators, + "service", + EV_SERVICE_NONE); + break; + } + + net.status = status; + } + + net.lac = lac; + net.cell_id = cell_id; + + if (net.operator_code != operator_code || + net.country_code != country_code) { + g_free(net.operator_name); + net.operator_name = NULL; + resolve_operator_name(operator_code, country_code); + net.operator_code = operator_code; + net.country_code = country_code; + } + + net.network_type = network_type; + net.supported_services = supported_services; +} + +static void handle_registration_status_change(DBusMessage *msg) +{ + uint8_t status; + dbus_uint16_t lac, network_type, supported_services; + dbus_uint32_t cell_id, operator_code, country_code; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_BYTE, &status, + DBUS_TYPE_UINT16, &lac, + DBUS_TYPE_UINT32, &cell_id, + DBUS_TYPE_UINT32, &operator_code, + DBUS_TYPE_UINT32, &country_code, + DBUS_TYPE_BYTE, &network_type, + DBUS_TYPE_BYTE, &supported_services, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in registration_status_change"); + return; + } + + update_registration_status(status, lac, cell_id, operator_code, + country_code, network_type, + supported_services); +} + +static void update_signal_strength(uint8_t signals_bar) +{ + int signal; + + if (signals_bar > 100) { + DBG("signals_bar greater than expected: %u", signals_bar); + signals_bar = 100; + } + + if (net.signals_bar == signals_bar) + return; + + /* A simple conversion from 0-100 to 0-5 (used by HFP) */ + signal = (signals_bar + 20) / 21; + + telephony_update_indicator(maemo_indicators, "signal", signal); + + net.signals_bar = signals_bar; + + DBG("Signal strength updated: %u/100, %d/5", signals_bar, signal); +} + +static void handle_signal_strength_change(DBusMessage *msg) +{ + uint8_t signals_bar, rssi_in_dbm; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_BYTE, &signals_bar, + DBUS_TYPE_BYTE, &rssi_in_dbm, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in signal_strength_change"); + return; + } + + update_signal_strength(signals_bar); +} + +static gboolean iter_get_basic_args(DBusMessageIter *iter, + int first_arg_type, ...) +{ + int type; + va_list ap; + + va_start(ap, first_arg_type); + + for (type = first_arg_type; type != DBUS_TYPE_INVALID; + type = va_arg(ap, int)) { + void *value = va_arg(ap, void *); + int real_type = dbus_message_iter_get_arg_type(iter); + + if (real_type != type) { + error("iter_get_basic_args: expected %c but got %c", + (char) type, (char) real_type); + break; + } + + dbus_message_iter_get_basic(iter, value); + dbus_message_iter_next(iter); + } + + va_end(ap); + + return type == DBUS_TYPE_INVALID ? TRUE : FALSE; +} + +static void hal_battery_level_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + dbus_int32_t level; + int *value = user_data; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("hald replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_error_init(&err); + if (dbus_message_get_args(reply, &err, + DBUS_TYPE_INT32, &level, + DBUS_TYPE_INVALID) == FALSE) { + error("Unable to parse GetPropertyInteger reply: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + *value = (int) level; + + if (value == &battchg_last) + DBG("telephony-maemo: battery.charge_level.last_full is %d", + *value); + else if (value == &battchg_design) + DBG("telephony-maemo: battery.charge_level.design is %d", + *value); + else + DBG("telephony-maemo: battery.charge_level.current is %d", + *value); + + if ((battchg_design > 0 || battchg_last > 0) && battchg_cur >= 0) { + int new, max; + + if (battchg_last > 0) + max = battchg_last; + else + max = battchg_design; + + new = battchg_cur * 5 / max; + + telephony_update_indicator(maemo_indicators, "battchg", new); + } +done: + dbus_message_unref(reply); +} + +static void hal_get_integer(const char *path, const char *key, void *user_data) +{ + send_method_call("org.freedesktop.Hal", path, + "org.freedesktop.Hal.Device", + "GetPropertyInteger", + hal_battery_level_reply, user_data, + DBUS_TYPE_STRING, &key, + DBUS_TYPE_INVALID); +} + +static void handle_hal_property_modified(DBusMessage *msg) +{ + DBusMessageIter iter, array; + dbus_int32_t num_changes; + const char *path; + + path = dbus_message_get_path(msg); + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INT32) { + error("Unexpected signature in hal PropertyModified signal"); + return; + } + + dbus_message_iter_get_basic(&iter, &num_changes); + dbus_message_iter_next(&iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in hal PropertyModified signal"); + return; + } + + dbus_message_iter_recurse(&iter, &array); + + while (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_INVALID) { + DBusMessageIter prop; + const char *name; + dbus_bool_t added, removed; + + dbus_message_iter_recurse(&array, &prop); + + if (!iter_get_basic_args(&prop, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_BOOLEAN, &added, + DBUS_TYPE_BOOLEAN, &removed, + DBUS_TYPE_INVALID)) { + error("Invalid hal PropertyModified parameters"); + break; + } + + if (g_str_equal(name, "battery.charge_level.last_full")) + hal_get_integer(path, name, &battchg_last); + else if (g_str_equal(name, "battery.charge_level.current")) + hal_get_integer(path, name, &battchg_cur); + else if (g_str_equal(name, "battery.charge_level.design")) + hal_get_integer(path, name, &battchg_design); + + dbus_message_iter_next(&array); + } +} + +static void csd_call_free(struct csd_call *call) +{ + if (!call) + return; + + g_free(call->object_path); + g_free(call->number); + + g_free(call); +} + +static void parse_call_list(DBusMessageIter *iter) +{ + do { + DBusMessageIter call_iter; + struct csd_call *call; + const char *object_path, *number; + dbus_uint32_t status; + dbus_bool_t originating, terminating, emerg, on_hold, conf; + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRUCT) { + error("Unexpected signature in GetCallInfoAll reply"); + break; + } + + dbus_message_iter_recurse(iter, &call_iter); + + if (!iter_get_basic_args(&call_iter, + DBUS_TYPE_OBJECT_PATH, &object_path, + DBUS_TYPE_UINT32, &status, + DBUS_TYPE_BOOLEAN, &originating, + DBUS_TYPE_BOOLEAN, &terminating, + DBUS_TYPE_BOOLEAN, &emerg, + DBUS_TYPE_BOOLEAN, &on_hold, + DBUS_TYPE_BOOLEAN, &conf, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) { + error("Parsing call D-Bus parameters failed"); + break; + } + + call = find_call(object_path); + if (!call) { + call = g_new0(struct csd_call, 1); + call->object_path = g_strdup(object_path); + call->status = (int) status; + calls = g_slist_append(calls, call); + DBG("telephony-maemo: new csd call instance at %s", + object_path); + } + + if (call->status == CSD_CALL_STATUS_IDLE) + continue; + + /* CSD gives incorrect call_hold property sometimes */ + if ((call->status != CSD_CALL_STATUS_HOLD && on_hold) || + (call->status == CSD_CALL_STATUS_HOLD && + !on_hold)) { + error("Conflicting call status and on_hold property!"); + on_hold = call->status == CSD_CALL_STATUS_HOLD; + } + + call->originating = originating; + call->on_hold = on_hold; + call->conference = conf; + g_free(call->number); + call->number = g_strdup(number); + + } while (dbus_message_iter_next(iter)); +} + +static void signal_strength_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + uint8_t signals_bar, rssi_in_dbm; + dbus_int32_t net_err; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("Unable to get signal strength: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_error_init(&err); + if (!dbus_message_get_args(reply, &err, + DBUS_TYPE_BYTE, &signals_bar, + DBUS_TYPE_BYTE, &rssi_in_dbm, + DBUS_TYPE_INT32, &net_err, + DBUS_TYPE_INVALID)) { + error("Unable to parse signal_strength reply: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + if (net_err != 0) { + error("get_signal_strength failed with code %d", net_err); + goto done; + } + + update_signal_strength(signals_bar); + +done: + dbus_message_unref(reply); +} + +static int get_signal_strength(void) +{ + return send_method_call(NETWORK_BUS_NAME, NETWORK_PATH, + NETWORK_INTERFACE, "get_signal_strength", + signal_strength_reply, NULL, + DBUS_TYPE_INVALID); +} + +static void registration_status_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + uint8_t status; + dbus_uint16_t lac, network_type, supported_services; + dbus_uint32_t cell_id, operator_code, country_code; + dbus_int32_t net_err; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("Unable to get registration status: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_error_init(&err); + if (!dbus_message_get_args(reply, &err, + DBUS_TYPE_BYTE, &status, + DBUS_TYPE_UINT16, &lac, + DBUS_TYPE_UINT32, &cell_id, + DBUS_TYPE_UINT32, &operator_code, + DBUS_TYPE_UINT32, &country_code, + DBUS_TYPE_BYTE, &network_type, + DBUS_TYPE_BYTE, &supported_services, + DBUS_TYPE_INT32, &net_err, + DBUS_TYPE_INVALID)) { + error("Unable to parse registration_status_change reply:" + " %s, %s", err.name, err.message); + dbus_error_free(&err); + goto done; + } + + if (net_err != 0) { + error("get_registration_status failed with code %d", net_err); + goto done; + } + + update_registration_status(status, lac, cell_id, operator_code, + country_code, network_type, + supported_services); + + get_signal_strength(); + +done: + dbus_message_unref(reply); +} + +static int get_registration_status(void) +{ + return send_method_call(NETWORK_BUS_NAME, NETWORK_PATH, + NETWORK_INTERFACE, "get_registration_status", + registration_status_reply, NULL, + DBUS_TYPE_INVALID); +} + +static void call_info_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, sub; + + get_calls_active = FALSE; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("csd replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in GetCallInfoAll return"); + goto done; + } + + dbus_message_iter_recurse(&iter, &sub); + + parse_call_list(&sub); + + get_registration_status(); + +done: + dbus_message_unref(reply); +} + +static void hal_find_device_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, sub; + const char *path; + char match_string[256]; + int type; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("hald replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in FindDeviceByCapability return"); + goto done; + } + + dbus_message_iter_recurse(&iter, &sub); + + type = dbus_message_iter_get_arg_type(&sub); + + if (type != DBUS_TYPE_OBJECT_PATH && type != DBUS_TYPE_STRING) { + error("No hal device with battery capability found"); + goto done; + } + + dbus_message_iter_get_basic(&sub, &path); + + DBG("telephony-maemo: found battery device at %s", path); + + snprintf(match_string, sizeof(match_string), + "type='signal'," + "path='%s'," + "interface='org.freedesktop.Hal.Device'," + "member='PropertyModified'", path); + dbus_bus_add_match(connection, match_string, NULL); + + hal_get_integer(path, "battery.charge_level.last_full", &battchg_last); + hal_get_integer(path, "battery.charge_level.current", &battchg_cur); + hal_get_integer(path, "battery.charge_level.design", &battchg_design); + +done: + dbus_message_unref(reply); +} + +static void phonebook_read_reply(DBusPendingCall *call, void *user_data) +{ + DBusError derr; + DBusMessage *reply; + const char *name, *number; + char **number_type = user_data; + dbus_int32_t current_location, err; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&derr); + if (dbus_set_error_from_message(&derr, reply)) { + error("SIM.Phonebook replied with an error: %s, %s", + derr.name, derr.message); + dbus_error_free(&derr); + goto done; + } + + dbus_error_init(&derr); + if (dbus_message_get_args(reply, &derr, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INT32, ¤t_location, + DBUS_TYPE_INT32, &err, + DBUS_TYPE_INVALID) == FALSE) { + error("Unable to parse SIM.Phonebook.read arguments: %s, %s", + derr.name, derr.message); + dbus_error_free(&derr); + goto done; + } + + if (err != 0) { + error("SIM.Phonebook.read failed with error %d", err); + if (number_type == &vmbx) + vmbx = g_strdup(getenv("VMBX_NUMBER")); + goto done; + } + + if (number_type == &msisdn) { + g_free(msisdn); + msisdn = g_strdup(number); + DBG("Got MSISDN %s (%s)", number, name); + } else { + g_free(vmbx); + vmbx = g_strdup(number); + DBG("Got voice mailbox number %s (%s)", number, name); + } + +done: + dbus_message_unref(reply); +} + +static void csd_init(void) +{ + dbus_uint32_t location; + uint8_t pb_type, location_type; + int ret; + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "GetCallInfoAll", + call_info_reply, NULL, DBUS_TYPE_INVALID); + if (ret < 0) { + error("Unable to sent GetCallInfoAll method call"); + return; + } + + get_calls_active = TRUE; + + pb_type = SIM_PHONEBOOK_TYPE_MSISDN; + location = PHONEBOOK_INDEX_FIRST_ENTRY; + location_type = SIM_PHONEBOOK_LOCATION_NEXT; + + ret = send_method_call(SIM_PHONEBOOK_BUS_NAME, SIM_PHONEBOOK_PATH, + SIM_PHONEBOOK_INTERFACE, "read", + phonebook_read_reply, &msisdn, + DBUS_TYPE_BYTE, &pb_type, + DBUS_TYPE_INT32, &location, + DBUS_TYPE_BYTE, &location_type, + DBUS_TYPE_INVALID); + if (ret < 0) { + error("Unable to send " SIM_PHONEBOOK_INTERFACE ".read()"); + return; + } + + pb_type = SIM_PHONEBOOK_TYPE_MBDN; + location = PHONEBOOK_INDEX_FIRST_ENTRY; + location_type = SIM_PHONEBOOK_LOCATION_NEXT; + + ret = send_method_call(SIM_PHONEBOOK_BUS_NAME, SIM_PHONEBOOK_PATH, + SIM_PHONEBOOK_INTERFACE, "read", + phonebook_read_reply, &vmbx, + DBUS_TYPE_BYTE, &pb_type, + DBUS_TYPE_INT32, &location, + DBUS_TYPE_BYTE, &location_type, + DBUS_TYPE_INVALID); + if (ret < 0) { + error("Unable to send " SIM_PHONEBOOK_INTERFACE ".read()"); + return; + } +} + +static uint32_t get_callflag(const char *callerid_setting) +{ + if (callerid_setting != NULL) { + if (g_str_equal(callerid_setting, "allowed")) + return CALL_FLAG_PRESENTATION_ALLOWED; + else if (g_str_equal(callerid_setting, "restricted")) + return CALL_FLAG_PRESENTATION_RESTRICTED; + else + return CALL_FLAG_NONE; + } else + return CALL_FLAG_NONE; +} + +static void generate_flag_file(const char *filename) +{ + int fd; + + if (g_file_test(ALLOWED_FLAG_FILE, G_FILE_TEST_EXISTS) || + g_file_test(RESTRICTED_FLAG_FILE, G_FILE_TEST_EXISTS) || + g_file_test(NONE_FLAG_FILE, G_FILE_TEST_EXISTS)) + return; + + fd = open(filename, O_WRONLY | O_CREAT, 0); + if (fd >= 0) + close(fd); +} + +static void save_callerid_to_file(const char *callerid_setting) +{ + char callerid_file[FILENAME_MAX]; + + snprintf(callerid_file, sizeof(callerid_file), "%s%s", + CALLERID_BASE, callerid_setting); + + if (g_file_test(ALLOWED_FLAG_FILE, G_FILE_TEST_EXISTS)) + rename(ALLOWED_FLAG_FILE, callerid_file); + else if (g_file_test(RESTRICTED_FLAG_FILE, G_FILE_TEST_EXISTS)) + rename(RESTRICTED_FLAG_FILE, callerid_file); + else if (g_file_test(NONE_FLAG_FILE, G_FILE_TEST_EXISTS)) + rename(NONE_FLAG_FILE, callerid_file); + else + generate_flag_file(callerid_file); +} + +static uint32_t callerid_from_file(void) +{ + if (g_file_test(ALLOWED_FLAG_FILE, G_FILE_TEST_EXISTS)) + return CALL_FLAG_PRESENTATION_ALLOWED; + else if (g_file_test(RESTRICTED_FLAG_FILE, G_FILE_TEST_EXISTS)) + return CALL_FLAG_PRESENTATION_RESTRICTED; + else if (g_file_test(NONE_FLAG_FILE, G_FILE_TEST_EXISTS)) + return CALL_FLAG_NONE; + else + return CALL_FLAG_NONE; +} + +static DBusMessage *set_callerid(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + const char *callerid_setting; + + if (dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, + &callerid_setting, + DBUS_TYPE_INVALID) == FALSE) + return btd_error_invalid_args(msg); + + if (g_str_equal(callerid_setting, "allowed") || + g_str_equal(callerid_setting, "restricted") || + g_str_equal(callerid_setting, "none")) { + save_callerid_to_file(callerid_setting); + callerid = get_callflag(callerid_setting); + DBG("telephony-maemo setting callerid flag: %s", + callerid_setting); + return dbus_message_new_method_return(msg); + } + + error("telephony-maemo: invalid argument %s for method call" + " SetCallerId", callerid_setting); + return btd_error_invalid_args(msg); +} + +static const GDBusMethodTable telephony_maemo_methods[] = { + { GDBUS_ASYNC_METHOD("SetCallerId", + GDBUS_ARGS({ "id", "s" }), NULL, + set_callerid) }, + { } +}; + +static void handle_modem_state(DBusMessage *msg) +{ + const char *state; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &state, + DBUS_TYPE_INVALID)) { + error("Unexpected modem state parameters"); + return; + } + + DBG("SSC modem state: %s", state); + + if (calls != NULL || get_calls_active) + return; + + if (g_str_equal(state, "cmt_ready") || g_str_equal(state, "online")) + csd_init(); +} + +static void modem_state_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + DBusError err; + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("get_modem_status: %s, %s", err.name, err.message); + dbus_error_free(&err); + } else + handle_modem_state(reply); + + dbus_message_unref(reply); +} + +static DBusHandlerResult signal_filter(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + const char *path = dbus_message_get_path(msg); + + if (dbus_message_get_type(msg) != DBUS_MESSAGE_TYPE_SIGNAL) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (dbus_message_is_signal(msg, CSD_CALL_INTERFACE, "Coming")) + handle_incoming_call(msg); + else if (dbus_message_is_signal(msg, CSD_CALL_INTERFACE, "Created")) + handle_outgoing_call(msg); + else if (dbus_message_is_signal(msg, CSD_CALL_INTERFACE, + "CreateRequested")) + handle_create_requested(msg); + else if (dbus_message_is_signal(msg, CSD_CALL_INSTANCE, "CallStatus")) + handle_call_status(msg, path); + else if (dbus_message_is_signal(msg, CSD_CALL_CONFERENCE, "Joined")) + handle_conference(msg, TRUE); + else if (dbus_message_is_signal(msg, CSD_CALL_CONFERENCE, "Left")) + handle_conference(msg, FALSE); + else if (dbus_message_is_signal(msg, NETWORK_INTERFACE, + "registration_status_change")) + handle_registration_status_change(msg); + else if (dbus_message_is_signal(msg, NETWORK_INTERFACE, + "signal_strength_change")) + handle_signal_strength_change(msg); + else if (dbus_message_is_signal(msg, "org.freedesktop.Hal.Device", + "PropertyModified")) + handle_hal_property_modified(msg); + else if (dbus_message_is_signal(msg, SSC_DBUS_IFACE, + "modem_state_changed_ind")) + handle_modem_state(msg); + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +int telephony_init(void) +{ + const char *battery_cap = "battery"; + uint32_t features = AG_FEATURE_EC_ANDOR_NR | + AG_FEATURE_INBAND_RINGTONE | + AG_FEATURE_REJECT_A_CALL | + AG_FEATURE_ENHANCED_CALL_STATUS | + AG_FEATURE_ENHANCED_CALL_CONTROL | + AG_FEATURE_EXTENDED_ERROR_RESULT_CODES | + AG_FEATURE_THREE_WAY_CALLING; + + connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + + if (!dbus_connection_add_filter(connection, signal_filter, + NULL, NULL)) + error("Can't add signal filter"); + + dbus_bus_add_match(connection, + "type=signal,interface=" CSD_CALL_INTERFACE, NULL); + dbus_bus_add_match(connection, + "type=signal,interface=" CSD_CALL_INSTANCE, NULL); + dbus_bus_add_match(connection, + "type=signal,interface=" CSD_CALL_CONFERENCE, NULL); + dbus_bus_add_match(connection, + "type=signal,interface=" NETWORK_INTERFACE, NULL); + dbus_bus_add_match(connection, + "type=signal,interface=" SSC_DBUS_IFACE + ",member=modem_state_changed_ind", NULL); + + if (send_method_call(SSC_DBUS_NAME, SSC_DBUS_PATH, SSC_DBUS_IFACE, + "get_modem_state", modem_state_reply, + NULL, DBUS_TYPE_INVALID) < 0) + error("Unable to send " SSC_DBUS_IFACE ".get_modem_state()"); + + generate_flag_file(NONE_FLAG_FILE); + callerid = callerid_from_file(); + + if (!g_dbus_register_interface(connection, TELEPHONY_MAEMO_PATH, + TELEPHONY_MAEMO_INTERFACE, telephony_maemo_methods, + NULL, NULL, NULL, NULL)) { + error("telephony-maemo interface %s init failed on path %s", + TELEPHONY_MAEMO_INTERFACE, TELEPHONY_MAEMO_PATH); + } + + DBG("telephony-maemo registering %s interface on path %s", + TELEPHONY_MAEMO_INTERFACE, TELEPHONY_MAEMO_PATH); + + telephony_ready_ind(features, maemo_indicators, BTRH_NOT_SUPPORTED, + chld_str); + if (send_method_call("org.freedesktop.Hal", + "/org/freedesktop/Hal/Manager", + "org.freedesktop.Hal.Manager", + "FindDeviceByCapability", + hal_find_device_reply, NULL, + DBUS_TYPE_STRING, &battery_cap, + DBUS_TYPE_INVALID) < 0) + error("Unable to send HAL method call"); + + return 0; +} + +void telephony_exit(void) +{ + g_slist_foreach(calls, (GFunc) csd_call_free, NULL); + g_slist_free(calls); + calls = NULL; + + dbus_connection_remove_filter(connection, signal_filter, NULL); + + dbus_connection_unref(connection); + connection = NULL; + + telephony_deinit(); +} diff --git a/audio/telephony-maemo6.c b/audio/telephony-maemo6.c new file mode 100644 index 0000000..0727ffe --- /dev/null +++ b/audio/telephony-maemo6.c @@ -0,0 +1,2200 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2008-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "log.h" +#include "telephony.h" +#include "error.h" + +/* SSC D-Bus definitions */ +#define SSC_DBUS_NAME "com.nokia.phone.SSC" +#define SSC_DBUS_IFACE "com.nokia.phone.SSC" +#define SSC_DBUS_PATH "/com/nokia/phone/SSC" + +/* libcsnet D-Bus definitions */ +#define CSD_CSNET_BUS_NAME "com.nokia.csd.CSNet" +#define CSD_CSNET_PATH "/com/nokia/csd/csnet" +#define CSD_CSNET_IFACE "com.nokia.csd.CSNet" +#define CSD_CSNET_REGISTRATION "com.nokia.csd.CSNet.NetworkRegistration" +#define CSD_CSNET_OPERATOR "com.nokia.csd.CSNet.NetworkOperator" +#define CSD_CSNET_SIGNAL "com.nokia.csd.CSNet.SignalStrength" + +enum net_registration_status { + NETWORK_REG_STATUS_HOME, + NETWORK_REG_STATUS_ROAMING, + NETWORK_REG_STATUS_OFFLINE, + NETWORK_REG_STATUS_SEARCHING, + NETWORK_REG_STATUS_NO_SIM, + NETWORK_REG_STATUS_POWEROFF, + NETWORK_REG_STATUS_POWERSAFE, + NETWORK_REG_STATUS_NO_COVERAGE, + NETWORK_REG_STATUS_REJECTED, + NETWORK_REG_STATUS_UNKOWN +}; + +/* CSD CALL plugin D-Bus definitions */ +#define CSD_CALL_BUS_NAME "com.nokia.csd.Call" +#define CSD_CALL_INTERFACE "com.nokia.csd.Call" +#define CSD_CALL_INSTANCE "com.nokia.csd.Call.Instance" +#define CSD_CALL_CONFERENCE "com.nokia.csd.Call.Conference" +#define CSD_CALL_PATH "/com/nokia/csd/call" +#define CSD_CALL_CONFERENCE_PATH "/com/nokia/csd/call/conference" + +/* Call status values as exported by the CSD CALL plugin */ +#define CSD_CALL_STATUS_IDLE 0 +#define CSD_CALL_STATUS_CREATE 1 +#define CSD_CALL_STATUS_COMING 2 +#define CSD_CALL_STATUS_PROCEEDING 3 +#define CSD_CALL_STATUS_MO_ALERTING 4 +#define CSD_CALL_STATUS_MT_ALERTING 5 +#define CSD_CALL_STATUS_WAITING 6 +#define CSD_CALL_STATUS_ANSWERED 7 +#define CSD_CALL_STATUS_ACTIVE 8 +#define CSD_CALL_STATUS_MO_RELEASE 9 +#define CSD_CALL_STATUS_MT_RELEASE 10 +#define CSD_CALL_STATUS_HOLD_INITIATED 11 +#define CSD_CALL_STATUS_HOLD 12 +#define CSD_CALL_STATUS_RETRIEVE_INITIATED 13 +#define CSD_CALL_STATUS_RECONNECT_PENDING 14 +#define CSD_CALL_STATUS_TERMINATED 15 +#define CSD_CALL_STATUS_SWAP_INITIATED 16 + +#define CALL_FLAG_NONE 0 +#define CALL_FLAG_PRESENTATION_ALLOWED 0x01 +#define CALL_FLAG_PRESENTATION_RESTRICTED 0x02 + +/* SIM Phonebook D-Bus definitions */ +#define CSD_SIMPB_BUS_NAME "com.nokia.csd.SIM" +#define CSD_SIMPB_INTERFACE "com.nokia.csd.SIM.Phonebook" +#define CSD_SIMPB_PATH "/com/nokia/csd/sim/phonebook" + +#define CSD_SIMPB_TYPE_ADN "ADN" +#define CSD_SIMPB_TYPE_FDN "FDN" +#define CSD_SIMPB_TYPE_SDN "SDN" +#define CSD_SIMPB_TYPE_VMBX "VMBX" +#define CSD_SIMPB_TYPE_MBDN "MBDN" +#define CSD_SIMPB_TYPE_EN "EN" +#define CSD_SIMPB_TYPE_MSISDN "MSISDN" + +/* OHM plugin D-Bus definitions */ +#define OHM_BUS_NAME "com.nokia.NonGraphicFeedback1" +#define OHM_INTERFACE "com.nokia.NonGraphicFeedback1" +#define OHM_PATH "/com/nokia/NonGraphicFeedback1" + +/* tone-genenerator D-Bus definitions */ +#define TONEGEN_BUS_NAME "com.Nokia.Telephony.Tones" +#define TONEGEN_INTERFACE "com.Nokia.Telephony.Tones" +#define TONEGEN_PATH "/com/Nokia/Telephony/Tones" + +/* tone-generator DTMF definitions */ +#define DTMF_ASTERISK 10 +#define DTMF_HASHMARK 11 +#define DTMF_A 12 +#define DTMF_B 13 +#define DTMF_C 14 +#define DTMF_D 15 + +#define FEEDBACK_TONE_DURATION 200 + +struct csd_call { + char *object_path; + int status; + gboolean originating; + gboolean emergency; + gboolean on_hold; + gboolean conference; + char *number; + gboolean setup; +}; + +static struct { + char *operator_name; + uint8_t status; + int32_t signal_bars; +} net = { + .operator_name = NULL, + .status = NETWORK_REG_STATUS_UNKOWN, + /* Init as 0 meaning inactive mode. In modem power off state + * can be be -1, but we treat all values as 0s regardless + * inactive or power off. */ + .signal_bars = 0, +}; + +struct pending_req { + DBusPendingCall *call; + void *user_data; +}; + +static int get_property(const char *iface, const char *prop); + +static DBusConnection *connection = NULL; + +static GSList *calls = NULL; +static GSList *watches = NULL; +static GSList *pending = NULL; + +/* Reference count for determining the call indicator status */ +static GSList *active_calls = NULL; + +/* Queue of DTMF tones to play */ +static GSList *tones = NULL; +static guint create_tones_timer = 0; + +static char *msisdn = NULL; /* Subscriber number */ +static char *vmbx = NULL; /* Voice mailbox number */ + +/* HAL battery namespace key values */ +static int battchg_cur = -1; /* "battery.charge_level.current" */ +static int battchg_last = -1; /* "battery.charge_level.last_full" */ +static int battchg_design = -1; /* "battery.charge_level.design" */ + +static gboolean get_calls_active = FALSE; + +static gboolean events_enabled = FALSE; + +/* Supported set of call hold operations */ +static const char *chld_str = "0,1,1x,2,2x,3,4"; + +/* Timer for tracking call creation requests */ +static guint create_request_timer = 0; + +static struct indicator maemo_indicators[] = +{ + { "battchg", "0-5", 5, TRUE }, + /* signal strength in terms of bars */ + { "signal", "0-5", 0, TRUE }, + { "service", "0,1", 0, TRUE }, + { "call", "0,1", 0, TRUE }, + { "callsetup", "0-3", 0, TRUE }, + { "callheld", "0-2", 0, FALSE }, + { "roam", "0,1", 0, TRUE }, + { NULL } +}; + +static char *call_status_str[] = { + "IDLE", + "CREATE", + "COMING", + "PROCEEDING", + "MO_ALERTING", + "MT_ALERTING", + "WAITING", + "ANSWERED", + "ACTIVE", + "MO_RELEASE", + "MT_RELEASE", + "HOLD_INITIATED", + "HOLD", + "RETRIEVE_INITIATED", + "RECONNECT_PENDING", + "TERMINATED", + "SWAP_INITIATED", + "???" +}; + +static int send_method_call(const char *dest, const char *path, + const char *interface, const char *method, + DBusPendingCallNotifyFunction cb, + void *user_data, int type, ...) +{ + DBusMessage *msg; + DBusPendingCall *call; + va_list args; + struct pending_req *req; + + msg = dbus_message_new_method_call(dest, path, interface, method); + if (!msg) { + error("Unable to allocate new D-Bus %s message", method); + return -ENOMEM; + } + + va_start(args, type); + + if (!dbus_message_append_args_valist(msg, type, args)) { + dbus_message_unref(msg); + va_end(args); + return -EIO; + } + + va_end(args); + + if (!cb) { + g_dbus_send_message(connection, msg); + return 0; + } + + if (!dbus_connection_send_with_reply(connection, msg, &call, -1)) { + error("Sending %s failed", method); + dbus_message_unref(msg); + return -EIO; + } + + dbus_pending_call_set_notify(call, cb, user_data, NULL); + + req = g_new0(struct pending_req, 1); + req->call = call; + req->user_data = user_data; + + pending = g_slist_prepend(pending, req); + dbus_message_unref(msg); + + return 0; +} + +static struct csd_call *find_call(const char *path) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (g_str_equal(call->object_path, path)) + return call; + } + + return NULL; +} + +static struct csd_call *find_non_held_call(void) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == CSD_CALL_STATUS_IDLE) + continue; + + if (call->status != CSD_CALL_STATUS_HOLD) + return call; + } + + return NULL; +} + +static struct csd_call *find_non_idle_call(void) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status != CSD_CALL_STATUS_IDLE) + return call; + } + + return NULL; +} + +static struct csd_call *find_call_with_status(int status) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == status) + return call; + } + + return NULL; +} + +static int release_conference(void) +{ + DBusMessage *msg; + + DBG("telephony-maemo6: releasing conference call"); + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + CSD_CALL_CONFERENCE_PATH, + CSD_CALL_INSTANCE, + "Release"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int release_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + call->object_path, + CSD_CALL_INSTANCE, + "Release"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int answer_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + call->object_path, + CSD_CALL_INSTANCE, + "Answer"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static struct pending_req *find_request(const DBusPendingCall *call) +{ + GSList *l; + + for (l = pending; l; l = l->next) { + struct pending_req *req = l->data; + + if (req->call == call) + return req; + } + + return NULL; +} + +static void pending_req_finalize(void *data) +{ + struct pending_req *req = data; + + if (!dbus_pending_call_get_completed(req->call)) + dbus_pending_call_cancel(req->call); + + dbus_pending_call_unref(req->call); + g_free(req); +} + +static void remove_pending(DBusPendingCall *call) +{ + struct pending_req *req = find_request(call); + + pending = g_slist_remove(pending, req); + pending_req_finalize(req); +} + +static void stop_ringtone_reply(DBusPendingCall *call, void *user_data) +{ + struct csd_call *coming = user_data; + + remove_pending(call); + answer_call(coming); +} + +static int stop_ringtone_and_answer(struct csd_call *call) +{ + int ret; + + ret = send_method_call(OHM_BUS_NAME, OHM_PATH, + OHM_INTERFACE, "StopRingtone", + stop_ringtone_reply, call, + DBUS_TYPE_INVALID); + if (ret < 0) + return answer_call(call); + + return 0; +} + +static int split_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, + call->object_path, + CSD_CALL_INSTANCE, + "Split"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int unhold_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Unhold"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int hold_call(struct csd_call *call) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Hold"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int swap_calls(void) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Swap"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int create_conference(void) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Conference"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int call_transfer(void) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, + "Transfer"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + g_dbus_send_message(connection, msg); + + return 0; +} + +static int number_type(const char *number) +{ + if (number == NULL) + return NUMBER_TYPE_TELEPHONY; + + if (number[0] == '+' || strncmp(number, "00", 2) == 0) + return NUMBER_TYPE_INTERNATIONAL; + + return NUMBER_TYPE_TELEPHONY; +} + +void telephony_device_connected(void *telephony_device) +{ + struct csd_call *coming; + + DBG("telephony-maemo6: device %p connected", telephony_device); + + coming = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + if (coming) { + if (find_call_with_status(CSD_CALL_STATUS_ACTIVE)) + telephony_call_waiting_ind(coming->number, + number_type(coming->number)); + else + telephony_incoming_call_ind(coming->number, + number_type(coming->number)); + } +} + +static void remove_pending_by_data(gpointer data, gpointer user_data) +{ + struct pending_req *req = data; + + if (req->user_data == user_data) { + pending = g_slist_remove(pending, req); + pending_req_finalize(req); + } +} + +void telephony_device_disconnected(void *telephony_device) +{ + DBG("telephony-maemo6: device %p disconnected", telephony_device); + events_enabled = FALSE; + + g_slist_foreach(pending, remove_pending_by_data, telephony_device); +} + +void telephony_event_reporting_req(void *telephony_device, int ind) +{ + events_enabled = ind == 1 ? TRUE : FALSE; + + telephony_event_reporting_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_response_and_hold_req(void *telephony_device, int rh) +{ + telephony_response_and_hold_rsp(telephony_device, + CME_ERROR_NOT_SUPPORTED); +} + +void telephony_terminate_call_req(void *telephony_device) +{ + struct csd_call *call; + struct csd_call *alerting; + int err; + + call = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + if (!call) + call = find_non_idle_call(); + + if (!call) { + error("No active call"); + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + alerting = find_call_with_status(CSD_CALL_STATUS_MO_ALERTING); + if (call->on_hold && alerting) + err = release_call(alerting); + else if (call->conference) + err = release_conference(); + else + err = release_call(call); + + if (err < 0) + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_terminate_call_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_answer_call_req(void *telephony_device) +{ + struct csd_call *call; + + call = find_call_with_status(CSD_CALL_STATUS_COMING); + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_PROCEEDING); + + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_WAITING); + + if (!call) { + telephony_answer_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + if (stop_ringtone_and_answer(call) < 0) + telephony_answer_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_answer_call_rsp(telephony_device, CME_ERROR_NONE); +} + +static void create_call_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + void *telephony_device = user_data; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("csd replied with an error: %s, %s", + err.name, err.message); + if (g_strcmp0(err.name, + "com.nokia.csd.Call.Error.CSInactive") == 0) + telephony_dial_number_rsp(telephony_device, + CME_ERROR_NO_NETWORK_SERVICE); + else + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + dbus_error_free(&err); + } else + telephony_dial_number_rsp(telephony_device, CME_ERROR_NONE); + + dbus_message_unref(reply); + remove_pending(call); +} + +void telephony_last_dialed_number_req(void *telephony_device) +{ + int ret; + + DBG("telephony-maemo6: last dialed number request"); + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "CreateFromLast", + create_call_reply, telephony_device, + DBUS_TYPE_INVALID); + if (ret < 0) + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); +} + +static const char *memory_dial_lookup(int location) +{ + if (location == 1) + return vmbx; + else + return NULL; +} + +void telephony_dial_number_req(void *telephony_device, const char *number) +{ + int ret; + + DBG("telephony-maemo6: dial request to %s", number); + + if (strncmp(number, "*31#", 4) == 0) + number += 4; + else if (strncmp(number, "#31#", 4) == 0) + number += 4; + else if (number[0] == '>') { + const char *location = &number[1]; + + number = memory_dial_lookup(strtol(&number[1], NULL, 0)); + if (!number) { + error("No number at memory location %s", location); + telephony_dial_number_rsp(telephony_device, + CME_ERROR_INVALID_INDEX); + return; + } + } + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "Create", + create_call_reply, telephony_device, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID); + if (ret < 0) + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); +} + +static void start_dtmf_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("csd replied with an error: %s, %s", + err.name, err.message); + + dbus_error_free(&err); + } else + send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "StopDTMF", + NULL, NULL, + DBUS_TYPE_INVALID); + + dbus_message_unref(reply); + remove_pending(call); +} + +static void start_dtmf(void *telephony_device, char tone) +{ + int ret; + + /* + * Stop tone immediately, modem will place it in queue and play + * required time. + */ + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "StartDTMF", + start_dtmf_reply, NULL, + DBUS_TYPE_BYTE, &tone, + DBUS_TYPE_INVALID); + if (ret < 0) { + telephony_transmit_dtmf_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_transmit_dtmf_rsp(telephony_device, CME_ERROR_NONE); +} + +static int tonegen_startevent(char tone) +{ + int ret; + dbus_uint32_t event_tone; + dbus_int32_t dbm0 = -15; + dbus_uint32_t duration = 150; + + switch (tone) { + case '*': + event_tone = DTMF_ASTERISK; + break; + case '#': + event_tone = DTMF_HASHMARK; + break; + case 'A': + event_tone = DTMF_A; + break; + case 'B': + event_tone = DTMF_B; + break; + case 'C': + event_tone = DTMF_C; + break; + case 'D': + event_tone = DTMF_D; + break; + default: + ret = g_ascii_digit_value(tone); + if (ret < 0) + return -EINVAL; + event_tone = ret; + } + + ret = send_method_call(TONEGEN_BUS_NAME, TONEGEN_PATH, + TONEGEN_INTERFACE, "StartEventTone", + NULL, NULL, + DBUS_TYPE_UINT32, &event_tone, + DBUS_TYPE_INT32, &dbm0, + DBUS_TYPE_UINT32, &duration, + DBUS_TYPE_INVALID); + return ret; +} + +static gboolean stop_feedback_tone(gpointer user_data) +{ + if (g_slist_length(tones) > 0) { + gpointer ptone; + int ret; + + send_method_call(TONEGEN_BUS_NAME, TONEGEN_PATH, + TONEGEN_INTERFACE, "StopTone", + NULL, NULL, + DBUS_TYPE_INVALID); + + ptone = g_slist_nth_data(tones, 0); + tones = g_slist_remove(tones, ptone); + + ret = tonegen_startevent(GPOINTER_TO_UINT(ptone)); + if (ret < 0) + goto done; + + return TRUE; + } +done: + return FALSE; +} + +static void tones_timer_notify(gpointer data) +{ + send_method_call(TONEGEN_BUS_NAME, TONEGEN_PATH, + TONEGEN_INTERFACE, "StopTone", + NULL, NULL, + DBUS_TYPE_INVALID); + g_slist_free(tones); + tones = NULL; + + create_tones_timer = 0; +} + +static void start_feedback_tone(char tone) +{ + if (!create_tones_timer) { + int ret; + + ret = tonegen_startevent(tone); + if (ret < 0) + return; + + create_tones_timer = g_timeout_add_full(G_PRIORITY_DEFAULT, + FEEDBACK_TONE_DURATION, + stop_feedback_tone, + NULL, + tones_timer_notify); + } else { + glong dtmf_tone = tone; + + DBG("add %c to queue", tone); + tones = g_slist_append(tones, GUINT_TO_POINTER(dtmf_tone)); + } +} + +void telephony_transmit_dtmf_req(void *telephony_device, char tone) +{ + DBG("telephony-maemo6: transmit dtmf: %c", tone); + + start_dtmf(telephony_device, tone); + + if (!find_call_with_status(CSD_CALL_STATUS_ACTIVE)) + error("No active call"); + else + start_feedback_tone(tone); +} + +void telephony_subscriber_number_req(void *telephony_device) +{ + DBG("telephony-maemo6: subscriber number request"); + if (msisdn) + telephony_subscriber_number_ind(msisdn, + number_type(msisdn), + SUBSCRIBER_SERVICE_VOICE); + telephony_subscriber_number_rsp(telephony_device, CME_ERROR_NONE); +} + +static int csd_status_to_hfp(struct csd_call *call) +{ + switch (call->status) { + case CSD_CALL_STATUS_IDLE: + case CSD_CALL_STATUS_MO_RELEASE: + case CSD_CALL_STATUS_MT_RELEASE: + case CSD_CALL_STATUS_TERMINATED: + return -1; + case CSD_CALL_STATUS_CREATE: + return CALL_STATUS_DIALING; + case CSD_CALL_STATUS_WAITING: + return CALL_STATUS_WAITING; + case CSD_CALL_STATUS_PROCEEDING: + /* PROCEEDING can happen in outgoing/incoming */ + if (call->originating) + return CALL_STATUS_DIALING; + + /* + * PROCEEDING is followed by WAITING CSD status, therefore + * second incoming call status indication is set immediately + * to waiting. + */ + if (g_slist_length(active_calls) > 0) + return CALL_STATUS_WAITING; + + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_COMING: + if (g_slist_length(active_calls) > 0) + return CALL_STATUS_WAITING; + + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_MO_ALERTING: + return CALL_STATUS_ALERTING; + case CSD_CALL_STATUS_MT_ALERTING: + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_ANSWERED: + case CSD_CALL_STATUS_ACTIVE: + case CSD_CALL_STATUS_RECONNECT_PENDING: + case CSD_CALL_STATUS_SWAP_INITIATED: + case CSD_CALL_STATUS_HOLD_INITIATED: + return CALL_STATUS_ACTIVE; + case CSD_CALL_STATUS_RETRIEVE_INITIATED: + case CSD_CALL_STATUS_HOLD: + return CALL_STATUS_HELD; + default: + return -1; + } +} + +void telephony_list_current_calls_req(void *telephony_device) +{ + GSList *l; + int i; + + DBG("telephony-maemo6: list current calls request"); + + for (l = calls, i = 1; l != NULL; l = l->next, i++) { + struct csd_call *call = l->data; + int status, direction, multiparty; + + status = csd_status_to_hfp(call); + if (status < 0) + continue; + + direction = call->originating ? + CALL_DIR_OUTGOING : CALL_DIR_INCOMING; + + multiparty = call->conference ? + CALL_MULTIPARTY_YES : CALL_MULTIPARTY_NO; + + telephony_list_current_call_ind(i, direction, status, + CALL_MODE_VOICE, multiparty, + call->number, + number_type(call->number)); + } + + telephony_list_current_calls_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_operator_selection_req(void *telephony_device) +{ + telephony_operator_selection_ind(OPERATOR_MODE_AUTO, + net.operator_name ? net.operator_name : ""); + telephony_operator_selection_rsp(telephony_device, CME_ERROR_NONE); +} + +static void foreach_call_with_status(int status, + int (*func)(struct csd_call *call)) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == status) + func(call); + } +} + +void telephony_call_hold_req(void *telephony_device, const char *cmd) +{ + const char *idx; + struct csd_call *call; + int err = 0; + + DBG("telephony-maemo6: got call hold request %s", cmd); + + if (strlen(cmd) > 1) + idx = &cmd[1]; + else + idx = NULL; + + if (idx) + call = g_slist_nth_data(calls, strtol(idx, NULL, 0) - 1); + else + call = NULL; + + switch (cmd[0]) { + case '0': + if (find_call_with_status(CSD_CALL_STATUS_WAITING)) + foreach_call_with_status(CSD_CALL_STATUS_WAITING, + release_call); + else + foreach_call_with_status(CSD_CALL_STATUS_HOLD, + release_call); + break; + case '1': + if (idx) { + if (call) + err = release_call(call); + break; + } + foreach_call_with_status(CSD_CALL_STATUS_ACTIVE, release_call); + call = find_call_with_status(CSD_CALL_STATUS_WAITING); + if (call) + err = answer_call(call); + break; + case '2': + if (idx) { + if (call) + err = split_call(call); + } else { + struct csd_call *held, *wait; + + call = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + held = find_call_with_status(CSD_CALL_STATUS_HOLD); + wait = find_call_with_status(CSD_CALL_STATUS_WAITING); + + if (wait) + err = answer_call(wait); + else if (call && held) + err = swap_calls(); + else { + if (call) + err = hold_call(call); + if (held) + err = unhold_call(held); + } + } + break; + case '3': + if (find_call_with_status(CSD_CALL_STATUS_HOLD) || + find_call_with_status(CSD_CALL_STATUS_WAITING)) + err = create_conference(); + break; + case '4': + err = call_transfer(); + break; + default: + DBG("Unknown call hold request"); + break; + } + + if (err) + telephony_call_hold_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_call_hold_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_nr_and_ec_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-maemo6: got %s NR and EC request", + enable ? "enable" : "disable"); + telephony_nr_and_ec_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_key_press_req(void *telephony_device, const char *keys) +{ + struct csd_call *active, *waiting; + int err; + + DBG("telephony-maemo6: got key press request for %s", keys); + + waiting = find_call_with_status(CSD_CALL_STATUS_COMING); + if (!waiting) + waiting = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + if (!waiting) + waiting = find_call_with_status(CSD_CALL_STATUS_PROCEEDING); + + active = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + + if (waiting) + err = answer_call(waiting); + else if (active) + err = release_call(active); + else + err = 0; + + if (err < 0) + telephony_key_press_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_key_press_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_voice_dial_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-maemo6: got %s voice dial request", + enable ? "enable" : "disable"); + + telephony_voice_dial_rsp(telephony_device, CME_ERROR_NOT_SUPPORTED); +} + +static void handle_incoming_call(DBusMessage *msg) +{ + const char *number, *call_path; + struct csd_call *call; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &call_path, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Call.Coming() signal"); + return; + } + + call = find_call(call_path); + if (!call) { + error("Didn't find any matching call object for %s", + call_path); + return; + } + + DBG("Incoming call to %s from number %s", call_path, number); + + g_free(call->number); + call->number = g_strdup(number); + + if (find_call_with_status(CSD_CALL_STATUS_ACTIVE) || + find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_call_waiting_ind(call->number, + number_type(call->number)); + else + telephony_incoming_call_ind(call->number, + number_type(call->number)); + + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_INCOMING); +} + +static void handle_outgoing_call(DBusMessage *msg) +{ + const char *number, *call_path; + struct csd_call *call; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &call_path, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Call.Created() signal"); + return; + } + + call = find_call(call_path); + if (!call) { + error("Didn't find any matching call object for %s", + call_path); + return; + } + + DBG("Outgoing call from %s to number %s", call_path, number); + + g_free(call->number); + call->number = g_strdup(number); + + if (create_request_timer) { + g_source_remove(create_request_timer); + create_request_timer = 0; + } +} + +static gboolean create_timeout(gpointer user_data) +{ + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_INACTIVE); + create_request_timer = 0; + return FALSE; +} + +static void handle_create_requested(DBusMessage *msg) +{ + DBG("Call.CreateRequested()"); + + if (create_request_timer) + g_source_remove(create_request_timer); + + create_request_timer = g_timeout_add_seconds(5, create_timeout, NULL); + + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); +} + +static void call_set_status(struct csd_call *call, dbus_uint32_t status) +{ + dbus_uint32_t prev_status; + int callheld = telephony_get_indicator(maemo_indicators, "callheld"); + + prev_status = call->status; + DBG("Call %s changed from %s to %s", call->object_path, + call_status_str[prev_status], call_status_str[status]); + + if (prev_status == status) { + DBG("Ignoring CSD Call state change to existing state"); + return; + } + + call->status = (int) status; + + switch (status) { + case CSD_CALL_STATUS_IDLE: + if (call->setup) { + telephony_update_indicator(maemo_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + if (!call->originating) + telephony_calling_stopped_ind(); + } + + g_free(call->number); + call->number = NULL; + call->originating = FALSE; + call->emergency = FALSE; + call->on_hold = FALSE; + call->conference = FALSE; + call->setup = FALSE; + break; + case CSD_CALL_STATUS_CREATE: + call->originating = TRUE; + call->setup = TRUE; + break; + case CSD_CALL_STATUS_COMING: + call->originating = FALSE; + call->setup = TRUE; + break; + case CSD_CALL_STATUS_PROCEEDING: + break; + case CSD_CALL_STATUS_MO_ALERTING: + telephony_update_indicator(maemo_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + break; + case CSD_CALL_STATUS_MT_ALERTING: + /* Some headsets expect incoming call notification before they + * can send ATA command. When call changed status from waiting + * to alerting we need to send missing notification. Otherwise + * headsets like Nokia BH-108 or BackBeat 903 are unable to + * answer incoming call that was previously waiting. */ + if (prev_status == CSD_CALL_STATUS_WAITING) + telephony_incoming_call_ind(call->number, + number_type(call->number)); + break; + case CSD_CALL_STATUS_WAITING: + break; + case CSD_CALL_STATUS_ANSWERED: + break; + case CSD_CALL_STATUS_ACTIVE: + if (call->on_hold) { + call->on_hold = FALSE; + if (find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + else + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_NONE); + } else { + if (!g_slist_find(active_calls, call)) + active_calls = g_slist_prepend(active_calls, call); + if (g_slist_length(active_calls) == 1) + telephony_update_indicator(maemo_indicators, + "call", + EV_CALL_ACTIVE); + /* Upgrade callheld status if necessary */ + if (callheld == EV_CALLHELD_ON_HOLD) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + telephony_update_indicator(maemo_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + if (!call->originating) + telephony_calling_stopped_ind(); + call->setup = FALSE; + } + break; + case CSD_CALL_STATUS_MO_RELEASE: + case CSD_CALL_STATUS_MT_RELEASE: + active_calls = g_slist_remove(active_calls, call); + if (g_slist_length(active_calls) == 0) + telephony_update_indicator(maemo_indicators, "call", + EV_CALL_INACTIVE); + + if (create_tones_timer) + g_source_remove(create_tones_timer); + break; + case CSD_CALL_STATUS_HOLD_INITIATED: + break; + case CSD_CALL_STATUS_HOLD: + call->on_hold = TRUE; + if (find_non_held_call()) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + else + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_ON_HOLD); + break; + case CSD_CALL_STATUS_RETRIEVE_INITIATED: + break; + case CSD_CALL_STATUS_RECONNECT_PENDING: + break; + case CSD_CALL_STATUS_TERMINATED: + if (call->on_hold && + !find_call_with_status(CSD_CALL_STATUS_HOLD)) { + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_NONE); + return; + } + + if (callheld == EV_CALLHELD_MULTIPLE && + find_call_with_status(CSD_CALL_STATUS_HOLD) && + !find_call_with_status(CSD_CALL_STATUS_ACTIVE)) + telephony_update_indicator(maemo_indicators, + "callheld", + EV_CALLHELD_ON_HOLD); + break; + case CSD_CALL_STATUS_SWAP_INITIATED: + break; + default: + error("Unknown call status %u", status); + break; + } +} + +static void handle_call_status(DBusMessage *msg, const char *call_path) +{ + struct csd_call *call; + dbus_uint32_t status, cause_type, cause; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_UINT32, &status, + DBUS_TYPE_UINT32, &cause_type, + DBUS_TYPE_UINT32, &cause, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Instance.CallStatus() signal"); + return; + } + + call = find_call(call_path); + if (!call) { + error("Didn't find any matching call object for %s", + call_path); + return; + } + + if (status > 16) { + error("Invalid call status %u", status); + return; + } + + call_set_status(call, status); +} + +static void handle_conference(DBusMessage *msg, gboolean joined) +{ + const char *path; + struct csd_call *call; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in Conference.%s", + dbus_message_get_member(msg)); + return; + } + + call = find_call(path); + if (!call) { + error("Conference signal for unknown call %s", path); + return; + } + + DBG("Call %s %s the conference", path, joined ? "joined" : "left"); + + call->conference = joined; +} + +static uint8_t str2status(const char *state) +{ + if (g_strcmp0(state, "Home") == 0) + return NETWORK_REG_STATUS_HOME; + else if (g_strcmp0(state, "Roaming") == 0) + return NETWORK_REG_STATUS_ROAMING; + else if (g_strcmp0(state, "Offline") == 0) + return NETWORK_REG_STATUS_OFFLINE; + else if (g_strcmp0(state, "Searching") == 0) + return NETWORK_REG_STATUS_SEARCHING; + else if (g_strcmp0(state, "NoSim") == 0) + return NETWORK_REG_STATUS_NO_SIM; + else if (g_strcmp0(state, "Poweroff") == 0) + return NETWORK_REG_STATUS_POWEROFF; + else if (g_strcmp0(state, "Powersafe") == 0) + return NETWORK_REG_STATUS_POWERSAFE; + else if (g_strcmp0(state, "NoCoverage") == 0) + return NETWORK_REG_STATUS_NO_COVERAGE; + else if (g_strcmp0(state, "Reject") == 0) + return NETWORK_REG_STATUS_REJECTED; + else + return NETWORK_REG_STATUS_UNKOWN; +} + +static void update_registration_status(const char *status) +{ + uint8_t new_status; + + new_status = str2status(status); + + if (net.status == new_status) + return; + + switch (new_status) { + case NETWORK_REG_STATUS_HOME: + telephony_update_indicator(maemo_indicators, "roam", + EV_ROAM_INACTIVE); + if (net.status > NETWORK_REG_STATUS_ROAMING) + telephony_update_indicator(maemo_indicators, + "service", + EV_SERVICE_PRESENT); + break; + case NETWORK_REG_STATUS_ROAMING: + telephony_update_indicator(maemo_indicators, "roam", + EV_ROAM_ACTIVE); + if (net.status > NETWORK_REG_STATUS_ROAMING) + telephony_update_indicator(maemo_indicators, + "service", + EV_SERVICE_PRESENT); + break; + case NETWORK_REG_STATUS_OFFLINE: + case NETWORK_REG_STATUS_SEARCHING: + case NETWORK_REG_STATUS_NO_SIM: + case NETWORK_REG_STATUS_POWEROFF: + case NETWORK_REG_STATUS_POWERSAFE: + case NETWORK_REG_STATUS_NO_COVERAGE: + case NETWORK_REG_STATUS_REJECTED: + case NETWORK_REG_STATUS_UNKOWN: + if (net.status < NETWORK_REG_STATUS_OFFLINE) + telephony_update_indicator(maemo_indicators, + "service", + EV_SERVICE_NONE); + break; + } + + net.status = new_status; + + DBG("telephony-maemo6: registration status changed: %s", status); +} + +static void handle_registration_changed(DBusMessage *msg) +{ + const char *status; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_STRING, &status, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in RegistrationChanged"); + return; + } + + update_registration_status(status); +} + +static void update_signal_strength(int32_t signal_bars) +{ + if (signal_bars < 0) { + DBG("signal strength smaller than expected: %d < 0", + signal_bars); + signal_bars = 0; + } else if (signal_bars > 5) { + DBG("signal strength greater than expected: %d > 5", + signal_bars); + signal_bars = 5; + } + + if (net.signal_bars == signal_bars) + return; + + telephony_update_indicator(maemo_indicators, "signal", signal_bars); + + net.signal_bars = signal_bars; + DBG("telephony-maemo6: signal strength updated: %d/5", signal_bars); +} + +static void handle_signal_bars_changed(DBusMessage *msg) +{ + int32_t signal_bars; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_INT32, &signal_bars, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in SignalBarsChanged"); + return; + } + + update_signal_strength(signal_bars); +} + +static gboolean iter_get_basic_args(DBusMessageIter *iter, + int first_arg_type, ...) +{ + int type; + va_list ap; + + va_start(ap, first_arg_type); + + for (type = first_arg_type; type != DBUS_TYPE_INVALID; + type = va_arg(ap, int)) { + void *value = va_arg(ap, void *); + int real_type = dbus_message_iter_get_arg_type(iter); + + if (real_type != type) { + error("iter_get_basic_args: expected %c but got %c", + (char) type, (char) real_type); + break; + } + + dbus_message_iter_get_basic(iter, value); + dbus_message_iter_next(iter); + } + + va_end(ap); + + return type == DBUS_TYPE_INVALID ? TRUE : FALSE; +} + +static void hal_battery_level_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + dbus_int32_t level; + int *value = user_data; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("hald replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + if (!dbus_message_get_args(reply, NULL, + DBUS_TYPE_INT32, &level, + DBUS_TYPE_INVALID)) { + error("Unexpected args in hald reply"); + goto done; + } + + *value = (int) level; + + if (value == &battchg_last) + DBG("telephony-maemo6: battery.charge_level.last_full is %d", + *value); + else if (value == &battchg_design) + DBG("telephony-maemo6: battery.charge_level.design is %d", + *value); + else + DBG("telephony-maemo6: battery.charge_level.current is %d", + *value); + + if ((battchg_design > 0 || battchg_last > 0) && battchg_cur >= 0) { + int new, max; + + if (battchg_last > 0) + max = battchg_last; + else + max = battchg_design; + + new = battchg_cur * 5 / max; + + telephony_update_indicator(maemo_indicators, "battchg", new); + } + +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static void hal_get_integer(const char *path, const char *key, void *user_data) +{ + send_method_call("org.freedesktop.Hal", path, + "org.freedesktop.Hal.Device", + "GetPropertyInteger", + hal_battery_level_reply, user_data, + DBUS_TYPE_STRING, &key, + DBUS_TYPE_INVALID); +} + +static void handle_hal_property_modified(DBusMessage *msg) +{ + DBusMessageIter iter, array; + dbus_int32_t num_changes; + const char *path; + + path = dbus_message_get_path(msg); + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INT32) { + error("Unexpected signature in hal PropertyModified signal"); + return; + } + + dbus_message_iter_get_basic(&iter, &num_changes); + dbus_message_iter_next(&iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in hal PropertyModified signal"); + return; + } + + dbus_message_iter_recurse(&iter, &array); + + while (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_INVALID) { + DBusMessageIter prop; + const char *name; + dbus_bool_t added, removed; + + dbus_message_iter_recurse(&array, &prop); + + if (!iter_get_basic_args(&prop, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_BOOLEAN, &added, + DBUS_TYPE_BOOLEAN, &removed, + DBUS_TYPE_INVALID)) { + error("Invalid hal PropertyModified parameters"); + break; + } + + if (g_str_equal(name, "battery.charge_level.last_full")) + hal_get_integer(path, name, &battchg_last); + else if (g_str_equal(name, "battery.charge_level.current")) + hal_get_integer(path, name, &battchg_cur); + else if (g_str_equal(name, "battery.charge_level.design")) + hal_get_integer(path, name, &battchg_design); + + dbus_message_iter_next(&array); + } +} + +static void csd_call_free(void *data) +{ + struct csd_call *call = data; + + if (!call) + return; + + g_free(call->object_path); + g_free(call->number); + + g_slist_foreach(pending, remove_pending_by_data, call); + + g_free(call); +} + +static void parse_call_list(DBusMessageIter *iter) +{ + do { + DBusMessageIter call_iter; + struct csd_call *call; + const char *object_path, *number; + dbus_uint32_t status; + dbus_bool_t originating, terminating, emerg, on_hold, conf; + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRUCT) { + error("Unexpected signature in GetCallInfoAll reply"); + break; + } + + dbus_message_iter_recurse(iter, &call_iter); + + if (!iter_get_basic_args(&call_iter, + DBUS_TYPE_OBJECT_PATH, &object_path, + DBUS_TYPE_UINT32, &status, + DBUS_TYPE_BOOLEAN, &originating, + DBUS_TYPE_BOOLEAN, &terminating, + DBUS_TYPE_BOOLEAN, &emerg, + DBUS_TYPE_BOOLEAN, &on_hold, + DBUS_TYPE_BOOLEAN, &conf, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_INVALID)) { + error("Parsing call D-Bus parameters failed"); + break; + } + + call = find_call(object_path); + if (!call) { + call = g_new0(struct csd_call, 1); + call->object_path = g_strdup(object_path); + calls = g_slist_append(calls, call); + DBG("telephony-maemo6: new csd call instance at %s", + object_path); + } + + if (status == CSD_CALL_STATUS_IDLE) + continue; + + /* CSD gives incorrect call_hold property sometimes */ + if ((call->status != CSD_CALL_STATUS_HOLD && on_hold) || + (call->status == CSD_CALL_STATUS_HOLD && + !on_hold)) { + error("Conflicting call status and on_hold property!"); + on_hold = call->status == CSD_CALL_STATUS_HOLD; + } + + call->originating = originating; + call->on_hold = on_hold; + call->conference = conf; + g_free(call->number); + call->number = g_strdup(number); + + /* Update indicators */ + call_set_status(call, status); + + } while (dbus_message_iter_next(iter)); +} + +static void update_operator_name(const char *name) +{ + if (name == NULL) + return; + + g_free(net.operator_name); + net.operator_name = g_strndup(name, 16); + DBG("telephony-maemo6: operator name updated: %s", name); +} + +static void get_property_reply(DBusPendingCall *call, void *user_data) +{ + char *prop = user_data; + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, sub; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("csd replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) { + error("Unexpected signature in Get return"); + goto done; + } + + dbus_message_iter_recurse(&iter, &sub); + + if (g_strcmp0(prop, "RegistrationStatus") == 0) { + const char *status; + + dbus_message_iter_get_basic(&sub, &status); + update_registration_status(status); + + get_property(CSD_CSNET_OPERATOR, "OperatorName"); + get_property(CSD_CSNET_SIGNAL, "SignalBars"); + } else if (g_strcmp0(prop, "OperatorName") == 0) { + const char *name; + + dbus_message_iter_get_basic(&sub, &name); + update_operator_name(name); + } else if (g_strcmp0(prop, "SignalBars") == 0) { + int32_t signal_bars; + + dbus_message_iter_get_basic(&sub, &signal_bars); + update_signal_strength(signal_bars); + } + +done: + g_free(prop); + dbus_message_unref(reply); + remove_pending(call); +} + +static int get_property(const char *iface, const char *prop) +{ + return send_method_call(CSD_CSNET_BUS_NAME, CSD_CSNET_PATH, + DBUS_INTERFACE_PROPERTIES, "Get", + get_property_reply, g_strdup(prop), + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &prop, + DBUS_TYPE_INVALID); +} + +static void handle_operator_name_changed(DBusMessage *msg) +{ + const char *name; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in OperatorNameChanged"); + return; + } + + update_operator_name(name); +} + +static void call_info_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, sub; + + get_calls_active = FALSE; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("csd replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in GetCallInfoAll return"); + goto done; + } + + dbus_message_iter_recurse(&iter, &sub); + + parse_call_list(&sub); + + get_property(CSD_CSNET_REGISTRATION, "RegistrationStatus"); + +done: + dbus_message_unref(reply); + remove_pending(call); +} + + +static void phonebook_read_reply(DBusPendingCall *call, void *user_data) +{ + DBusError derr; + DBusMessage *reply; + const char *name, *number, *secondname, *additionalnumber, *email; + int index; + char **number_type = user_data; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&derr); + if (dbus_set_error_from_message(&derr, reply)) { + error("%s.ReadFirst replied with an error: %s, %s", + CSD_SIMPB_INTERFACE, derr.name, derr.message); + dbus_error_free(&derr); + if (number_type == &vmbx) + vmbx = g_strdup(getenv("VMBX_NUMBER")); + goto done; + } + + dbus_error_init(&derr); + if (dbus_message_get_args(reply, NULL, + DBUS_TYPE_INT32, &index, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_STRING, &secondname, + DBUS_TYPE_STRING, &additionalnumber, + DBUS_TYPE_STRING, &email, + DBUS_TYPE_INVALID) == FALSE) { + error("Unable to parse %s.ReadFirst arguments: %s, %s", + CSD_SIMPB_INTERFACE, derr.name, derr.message); + dbus_error_free(&derr); + goto done; + } + + if (number_type == &msisdn) { + g_free(msisdn); + msisdn = g_strdup(number); + DBG("Got MSISDN %s (%s)", number, name); + } else { + g_free(vmbx); + vmbx = g_strdup(number); + DBG("Got voice mailbox number %s (%s)", number, name); + } + +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static void csd_init(void) +{ + const char *pb_type; + int ret; + + ret = send_method_call(CSD_CALL_BUS_NAME, CSD_CALL_PATH, + CSD_CALL_INTERFACE, "GetCallInfoAll", + call_info_reply, NULL, DBUS_TYPE_INVALID); + if (ret < 0) { + error("Unable to sent GetCallInfoAll method call"); + return; + } + + get_calls_active = TRUE; + + pb_type = CSD_SIMPB_TYPE_MSISDN; + + ret = send_method_call(CSD_SIMPB_BUS_NAME, CSD_SIMPB_PATH, + CSD_SIMPB_INTERFACE, "ReadFirst", + phonebook_read_reply, &msisdn, + DBUS_TYPE_STRING, &pb_type, + DBUS_TYPE_INVALID); + if (ret < 0) { + error("Unable to send " CSD_SIMPB_INTERFACE ".read()"); + return; + } + + /* Voicemail should be in MBDN index 0 */ + pb_type = CSD_SIMPB_TYPE_MBDN; + + ret = send_method_call(CSD_SIMPB_BUS_NAME, CSD_SIMPB_PATH, + CSD_SIMPB_INTERFACE, "ReadFirst", + phonebook_read_reply, &vmbx, + DBUS_TYPE_STRING, &pb_type, + DBUS_TYPE_INVALID); + if (ret < 0) { + error("Unable to send " CSD_SIMPB_INTERFACE ".read()"); + return; + } +} + +static void handle_modem_state(DBusMessage *msg) +{ + const char *state; + + if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &state, + DBUS_TYPE_INVALID)) { + error("Unexpected modem state parameters"); + return; + } + + DBG("SSC modem state: %s", state); + + if (calls != NULL || get_calls_active) + return; + + if (g_str_equal(state, "cmt_ready") || g_str_equal(state, "online")) + csd_init(); +} + +static void modem_state_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + DBusError err; + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("get_modem_state: %s, %s", err.name, err.message); + dbus_error_free(&err); + } else + handle_modem_state(reply); + + dbus_message_unref(reply); + remove_pending(call); +} + +static gboolean signal_filter(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + const char *path = dbus_message_get_path(msg); + + if (dbus_message_is_signal(msg, CSD_CALL_INTERFACE, "Coming")) + handle_incoming_call(msg); + else if (dbus_message_is_signal(msg, CSD_CALL_INTERFACE, "Created")) + handle_outgoing_call(msg); + else if (dbus_message_is_signal(msg, CSD_CALL_INTERFACE, + "CreateRequested")) + handle_create_requested(msg); + else if (dbus_message_is_signal(msg, CSD_CALL_INSTANCE, "CallStatus")) + handle_call_status(msg, path); + else if (dbus_message_is_signal(msg, CSD_CALL_CONFERENCE, "Joined")) + handle_conference(msg, TRUE); + else if (dbus_message_is_signal(msg, CSD_CALL_CONFERENCE, "Left")) + handle_conference(msg, FALSE); + else if (dbus_message_is_signal(msg, CSD_CSNET_REGISTRATION, + "RegistrationChanged")) + handle_registration_changed(msg); + else if (dbus_message_is_signal(msg, CSD_CSNET_OPERATOR, + "OperatorNameChanged")) + handle_operator_name_changed(msg); + else if (dbus_message_is_signal(msg, CSD_CSNET_SIGNAL, + "SignalBarsChanged")) + handle_signal_bars_changed(msg); + else if (dbus_message_is_signal(msg, "org.freedesktop.Hal.Device", + "PropertyModified")) + handle_hal_property_modified(msg); + else if (dbus_message_is_signal(msg, SSC_DBUS_IFACE, + "modem_state_changed_ind")) + handle_modem_state(msg); + + return TRUE; +} + +static void add_watch(const char *sender, const char *path, + const char *interface, const char *member) +{ + guint watch; + + watch = g_dbus_add_signal_watch(connection, sender, path, interface, + member, signal_filter, NULL, NULL); + + watches = g_slist_prepend(watches, GUINT_TO_POINTER(watch)); +} + +static void hal_find_device_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, sub; + const char *path; + int type; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("hald replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in FindDeviceByCapability return"); + goto done; + } + + dbus_message_iter_recurse(&iter, &sub); + + type = dbus_message_iter_get_arg_type(&sub); + + if (type != DBUS_TYPE_OBJECT_PATH && type != DBUS_TYPE_STRING) { + error("No hal device with battery capability found"); + goto done; + } + + dbus_message_iter_get_basic(&sub, &path); + + DBG("telephony-maemo6: found battery device at %s", path); + + add_watch(NULL, path, "org.freedesktop.Hal.Device", + "PropertyModified"); + + hal_get_integer(path, "battery.charge_level.last_full", &battchg_last); + hal_get_integer(path, "battery.charge_level.current", &battchg_cur); + hal_get_integer(path, "battery.charge_level.design", &battchg_design); + +done: + dbus_message_unref(reply); + remove_pending(call); +} + +int telephony_init(void) +{ + const char *battery_cap = "battery"; + uint32_t features = AG_FEATURE_EC_ANDOR_NR | + AG_FEATURE_INBAND_RINGTONE | + AG_FEATURE_REJECT_A_CALL | + AG_FEATURE_ENHANCED_CALL_STATUS | + AG_FEATURE_ENHANCED_CALL_CONTROL | + AG_FEATURE_EXTENDED_ERROR_RESULT_CODES | + AG_FEATURE_THREE_WAY_CALLING; + int i; + + DBG(""); + + connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + + add_watch(NULL, NULL, CSD_CALL_INTERFACE, NULL); + add_watch(NULL, NULL, CSD_CALL_INSTANCE, NULL); + add_watch(NULL, NULL, CSD_CALL_CONFERENCE, NULL); + add_watch(NULL, NULL, CSD_CSNET_REGISTRATION, "RegistrationChanged"); + add_watch(NULL, NULL, CSD_CSNET_OPERATOR, "OperatorNameChanged"); + add_watch(NULL, NULL, CSD_CSNET_SIGNAL, "SignalBarsChanged"); + add_watch(NULL, NULL, SSC_DBUS_IFACE, "modem_state_changed_ind"); + + if (send_method_call(SSC_DBUS_NAME, SSC_DBUS_PATH, SSC_DBUS_IFACE, + "get_modem_state", modem_state_reply, + NULL, DBUS_TYPE_INVALID) < 0) + error("Unable to send " SSC_DBUS_IFACE ".get_modem_state()"); + + /* Reset indicators */ + for (i = 0; maemo_indicators[i].desc != NULL; i++) { + if (g_str_equal(maemo_indicators[i].desc, "battchg")) + maemo_indicators[i].val = 5; + else + maemo_indicators[i].val = 0; + } + + telephony_ready_ind(features, maemo_indicators, BTRH_NOT_SUPPORTED, + chld_str); + if (send_method_call("org.freedesktop.Hal", + "/org/freedesktop/Hal/Manager", + "org.freedesktop.Hal.Manager", + "FindDeviceByCapability", + hal_find_device_reply, NULL, + DBUS_TYPE_STRING, &battery_cap, + DBUS_TYPE_INVALID) < 0) + error("Unable to send HAL method call"); + + return 0; +} + +static void remove_watch(gpointer data) +{ + g_dbus_remove_watch(connection, GPOINTER_TO_UINT(data)); +} + +void telephony_exit(void) +{ + DBG(""); + + g_free(net.operator_name); + net.operator_name = NULL; + + net.status = NETWORK_REG_STATUS_UNKOWN; + net.signal_bars = 0; + + g_slist_free(active_calls); + active_calls = NULL; + + g_slist_free_full(calls, csd_call_free); + calls = NULL; + + g_slist_free_full(pending, pending_req_finalize); + pending = NULL; + + g_slist_free_full(watches, remove_watch); + watches = NULL; + + dbus_connection_unref(connection); + connection = NULL; + + telephony_deinit(); +} diff --git a/audio/telephony-ofono.c b/audio/telephony-ofono.c new file mode 100644 index 0000000..961fedd --- /dev/null +++ b/audio/telephony-ofono.c @@ -0,0 +1,1637 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2009-2010 Intel Corporation + * Copyright (C) 2006-2009 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "log.h" +#include "telephony.h" + +enum net_registration_status { + NETWORK_REG_STATUS_HOME = 0x00, + NETWORK_REG_STATUS_ROAM, + NETWORK_REG_STATUS_NOSERV +}; + +struct voice_call { + char *obj_path; + int status; + gboolean originating; + gboolean conference; + char *number; + guint watch; +}; + +static DBusConnection *connection = NULL; +static char *modem_obj_path = NULL; +static char *last_dialed_number = NULL; +static GSList *calls = NULL; +static GSList *watches = NULL; +static GSList *pending = NULL; + +#define OFONO_BUS_NAME "org.ofono" +#define OFONO_PATH "/" +#define OFONO_MODEM_INTERFACE "org.ofono.Modem" +#define OFONO_MANAGER_INTERFACE "org.ofono.Manager" +#define OFONO_NETWORKREG_INTERFACE "org.ofono.NetworkRegistration" +#define OFONO_VCMANAGER_INTERFACE "org.ofono.VoiceCallManager" +#define OFONO_VC_INTERFACE "org.ofono.VoiceCall" + +/* HAL battery namespace key values */ +static int battchg_cur = -1; /* "battery.charge_level.current" */ +static int battchg_last = -1; /* "battery.charge_level.last_full" */ +static int battchg_design = -1; /* "battery.charge_level.design" */ + +static struct { + uint8_t status; + uint32_t signals_bar; + char *operator_name; +} net = { + .status = NETWORK_REG_STATUS_NOSERV, + .signals_bar = 0, + .operator_name = NULL, +}; + +static const char *chld_str = "0,1,1x,2,2x,3,4"; +static char *subscriber_number = NULL; + +static gboolean events_enabled = FALSE; + +static struct indicator ofono_indicators[] = +{ + { "battchg", "0-5", 5, TRUE }, + { "signal", "0-5", 5, TRUE }, + { "service", "0,1", 1, TRUE }, + { "call", "0,1", 0, TRUE }, + { "callsetup", "0-3", 0, TRUE }, + { "callheld", "0-2", 0, FALSE }, + { "roam", "0,1", 0, TRUE }, + { NULL } +}; + +static struct voice_call *find_vc(const char *path) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct voice_call *vc = l->data; + + if (g_str_equal(vc->obj_path, path)) + return vc; + } + + return NULL; +} + +static struct voice_call *find_vc_with_status(int status) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct voice_call *vc = l->data; + + if (vc->status == status) + return vc; + } + + return NULL; +} + +static struct voice_call *find_vc_without_status(int status) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct voice_call *call = l->data; + + if (call->status != status) + return call; + } + + return NULL; +} + +static int number_type(const char *number) +{ + if (number == NULL) + return NUMBER_TYPE_TELEPHONY; + + if (number[0] == '+' || strncmp(number, "00", 2) == 0) + return NUMBER_TYPE_INTERNATIONAL; + + return NUMBER_TYPE_TELEPHONY; +} + +void telephony_device_connected(void *telephony_device) +{ + struct voice_call *coming; + + DBG("telephony-ofono: device %p connected", telephony_device); + + coming = find_vc_with_status(CALL_STATUS_ALERTING); + if (coming) { + if (find_vc_with_status(CALL_STATUS_ACTIVE)) + telephony_call_waiting_ind(coming->number, + number_type(coming->number)); + else + telephony_incoming_call_ind(coming->number, + number_type(coming->number)); + } +} + +void telephony_device_disconnected(void *telephony_device) +{ + DBG("telephony-ofono: device %p disconnected", telephony_device); + events_enabled = FALSE; +} + +void telephony_event_reporting_req(void *telephony_device, int ind) +{ + events_enabled = ind == 1 ? TRUE : FALSE; + + telephony_event_reporting_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_response_and_hold_req(void *telephony_device, int rh) +{ + telephony_response_and_hold_rsp(telephony_device, + CME_ERROR_NOT_SUPPORTED); +} + +void telephony_last_dialed_number_req(void *telephony_device) +{ + DBG("telephony-ofono: last dialed number request"); + + if (last_dialed_number) + telephony_dial_number_req(telephony_device, last_dialed_number); + else + telephony_last_dialed_number_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); +} + +static int send_method_call(const char *dest, const char *path, + const char *interface, const char *method, + DBusPendingCallNotifyFunction cb, + void *user_data, int type, ...) +{ + DBusMessage *msg; + DBusPendingCall *call; + va_list args; + + msg = dbus_message_new_method_call(dest, path, interface, method); + if (!msg) { + error("Unable to allocate new D-Bus %s message", method); + return -ENOMEM; + } + + va_start(args, type); + + if (!dbus_message_append_args_valist(msg, type, args)) { + dbus_message_unref(msg); + va_end(args); + return -EIO; + } + + va_end(args); + + if (!cb) { + g_dbus_send_message(connection, msg); + return 0; + } + + if (!dbus_connection_send_with_reply(connection, msg, &call, -1)) { + error("Sending %s failed", method); + dbus_message_unref(msg); + return -EIO; + } + + dbus_pending_call_set_notify(call, cb, user_data, NULL); + pending = g_slist_prepend(pending, call); + dbus_message_unref(msg); + + return 0; +} + +static int answer_call(struct voice_call *vc) +{ + DBG("%s", vc->number); + return send_method_call(OFONO_BUS_NAME, vc->obj_path, + OFONO_VC_INTERFACE, "Answer", + NULL, NULL, DBUS_TYPE_INVALID); +} + +static int release_call(struct voice_call *vc) +{ + DBG("%s", vc->number); + return send_method_call(OFONO_BUS_NAME, vc->obj_path, + OFONO_VC_INTERFACE, "Hangup", + NULL, NULL, DBUS_TYPE_INVALID); +} + +static int release_answer_calls(void) +{ + DBG(""); + return send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "ReleaseAndAnswer", + NULL, NULL, DBUS_TYPE_INVALID); +} + +static int split_call(struct voice_call *call) +{ + DBG("%s", call->number); + return send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "PrivateChat", + NULL, NULL, + DBUS_TYPE_OBJECT_PATH, + call->obj_path, + DBUS_TYPE_INVALID); + return -1; +} + +static int swap_calls(void) +{ + DBG(""); + return send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "SwapCalls", + NULL, NULL, DBUS_TYPE_INVALID); +} + +static int create_conference(void) +{ + DBG(""); + return send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "CreateMultiparty", + NULL, NULL, DBUS_TYPE_INVALID); +} + +static int release_conference(void) +{ + DBG(""); + return send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "HangupMultiparty", + NULL, NULL, DBUS_TYPE_INVALID); +} + +static int call_transfer(void) +{ + DBG(""); + return send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "Transfer", + NULL, NULL, DBUS_TYPE_INVALID); +} + +void telephony_terminate_call_req(void *telephony_device) +{ + struct voice_call *call; + struct voice_call *alerting; + int err; + + call = find_vc_with_status(CALL_STATUS_ACTIVE); + if (!call) + call = calls->data; + + if (!call) { + error("No active call"); + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + alerting = find_vc_with_status(CALL_STATUS_ALERTING); + if (call->status == CALL_STATUS_HELD && alerting) + err = release_call(alerting); + else if (call->conference) + err = release_conference(); + else + err = release_call(call); + + if (err < 0) + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_terminate_call_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_answer_call_req(void *telephony_device) +{ + struct voice_call *vc; + int ret; + + vc = find_vc_with_status(CALL_STATUS_INCOMING); + if (!vc) + vc = find_vc_with_status(CALL_STATUS_ALERTING); + + if (!vc) + vc = find_vc_with_status(CALL_STATUS_WAITING); + + if (!vc) { + telephony_answer_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + ret = answer_call(vc); + if (ret < 0) { + telephony_answer_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_answer_call_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_dial_number_req(void *telephony_device, const char *number) +{ + const char *clir; + int ret; + + DBG("telephony-ofono: dial request to %s", number); + + if (!modem_obj_path) { + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + if (!strncmp(number, "*31#", 4)) { + number += 4; + clir = "enabled"; + } else if (!strncmp(number, "#31#", 4)) { + number += 4; + clir = "disabled"; + } else + clir = "default"; + + ret = send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "Dial", NULL, NULL, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_STRING, &clir, + DBUS_TYPE_INVALID); + + if (ret < 0) + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_dial_number_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_transmit_dtmf_req(void *telephony_device, char tone) +{ + char *tone_string; + int ret; + + DBG("telephony-ofono: transmit dtmf: %c", tone); + + if (!modem_obj_path) { + telephony_transmit_dtmf_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + tone_string = g_strdup_printf("%c", tone); + ret = send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, + "SendTones", NULL, NULL, + DBUS_TYPE_STRING, &tone_string, + DBUS_TYPE_INVALID); + g_free(tone_string); + + if (ret < 0) + telephony_transmit_dtmf_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_transmit_dtmf_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_subscriber_number_req(void *telephony_device) +{ + DBG("telephony-ofono: subscriber number request"); + + if (subscriber_number) + telephony_subscriber_number_ind(subscriber_number, + NUMBER_TYPE_TELEPHONY, + SUBSCRIBER_SERVICE_VOICE); + telephony_subscriber_number_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_list_current_calls_req(void *telephony_device) +{ + GSList *l; + int i; + + DBG("telephony-ofono: list current calls request"); + + for (l = calls, i = 1; l != NULL; l = l->next, i++) { + struct voice_call *vc = l->data; + int direction, multiparty; + + direction = vc->originating ? + CALL_DIR_OUTGOING : CALL_DIR_INCOMING; + + multiparty = vc->conference ? + CALL_MULTIPARTY_YES : CALL_MULTIPARTY_NO; + + DBG("call %s direction %d multiparty %d", vc->number, + direction, multiparty); + + telephony_list_current_call_ind(i, direction, vc->status, + CALL_MODE_VOICE, multiparty, + vc->number, number_type(vc->number)); + } + + telephony_list_current_calls_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_operator_selection_req(void *telephony_device) +{ + DBG("telephony-ofono: operator selection request"); + + telephony_operator_selection_ind(OPERATOR_MODE_AUTO, + net.operator_name ? net.operator_name : ""); + telephony_operator_selection_rsp(telephony_device, CME_ERROR_NONE); +} + +static void foreach_vc_with_status(int status, + int (*func)(struct voice_call *vc)) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct voice_call *call = l->data; + + if (call->status == status) + func(call); + } +} + +void telephony_call_hold_req(void *telephony_device, const char *cmd) +{ + const char *idx; + struct voice_call *call; + int err = 0; + + DBG("telephony-ofono: got call hold request %s", cmd); + + if (strlen(cmd) > 1) + idx = &cmd[1]; + else + idx = NULL; + + if (idx) + call = g_slist_nth_data(calls, strtol(idx, NULL, 0) - 1); + else + call = NULL; + + switch (cmd[0]) { + case '0': + if (find_vc_with_status(CALL_STATUS_WAITING)) + foreach_vc_with_status(CALL_STATUS_WAITING, + release_call); + else + foreach_vc_with_status(CALL_STATUS_HELD, release_call); + break; + case '1': + if (idx) { + if (call) + err = release_call(call); + break; + } + err = release_answer_calls(); + break; + case '2': + if (idx) { + if (call) + err = split_call(call); + } else { + call = find_vc_with_status(CALL_STATUS_WAITING); + + if (call) + err = answer_call(call); + else + err = swap_calls(); + } + break; + case '3': + if (find_vc_with_status(CALL_STATUS_HELD) || + find_vc_with_status(CALL_STATUS_WAITING)) + err = create_conference(); + break; + case '4': + err = call_transfer(); + break; + default: + DBG("Unknown call hold request"); + break; + } + + if (err) + telephony_call_hold_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_call_hold_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_nr_and_ec_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-ofono: got %s NR and EC request", + enable ? "enable" : "disable"); + + telephony_nr_and_ec_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_key_press_req(void *telephony_device, const char *keys) +{ + struct voice_call *active, *incoming; + int err; + + DBG("telephony-ofono: got key press request for %s", keys); + + incoming = find_vc_with_status(CALL_STATUS_INCOMING); + + active = find_vc_with_status(CALL_STATUS_ACTIVE); + + if (incoming) + err = answer_call(incoming); + else if (active) + err = release_call(active); + else + err = 0; + + if (err < 0) + telephony_key_press_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_key_press_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_voice_dial_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-ofono: got %s voice dial request", + enable ? "enable" : "disable"); + + telephony_voice_dial_rsp(telephony_device, CME_ERROR_NOT_SUPPORTED); +} + +static gboolean iter_get_basic_args(DBusMessageIter *iter, + int first_arg_type, ...) +{ + int type; + va_list ap; + + va_start(ap, first_arg_type); + + for (type = first_arg_type; type != DBUS_TYPE_INVALID; + type = va_arg(ap, int)) { + void *value = va_arg(ap, void *); + int real_type = dbus_message_iter_get_arg_type(iter); + + if (real_type != type) { + error("iter_get_basic_args: expected %c but got %c", + (char) type, (char) real_type); + break; + } + + dbus_message_iter_get_basic(iter, value); + dbus_message_iter_next(iter); + } + + va_end(ap); + + return type == DBUS_TYPE_INVALID ? TRUE : FALSE; +} + +static void call_free(void *data) +{ + struct voice_call *vc = data; + + DBG("%s", vc->obj_path); + + if (vc->status == CALL_STATUS_ACTIVE) + telephony_update_indicator(ofono_indicators, "call", + EV_CALL_INACTIVE); + else + telephony_update_indicator(ofono_indicators, "callsetup", + EV_CALLSETUP_INACTIVE); + + if (vc->status == CALL_STATUS_INCOMING) + telephony_calling_stopped_ind(); + + g_dbus_remove_watch(connection, vc->watch); + g_free(vc->obj_path); + g_free(vc->number); + g_free(vc); +} + +static gboolean handle_vc_property_changed(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + struct voice_call *vc = data; + const char *obj_path = dbus_message_get_path(msg); + DBusMessageIter iter, sub; + const char *property, *state; + + DBG("path %s", obj_path); + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) { + error("Unexpected signature in vc PropertyChanged signal"); + return TRUE; + } + + dbus_message_iter_get_basic(&iter, &property); + DBG("property %s", property); + + dbus_message_iter_next(&iter); + dbus_message_iter_recurse(&iter, &sub); + if (g_str_equal(property, "State")) { + dbus_message_iter_get_basic(&sub, &state); + DBG("State %s", state); + if (g_str_equal(state, "disconnected")) { + calls = g_slist_remove(calls, vc); + call_free(vc); + } else if (g_str_equal(state, "active")) { + telephony_update_indicator(ofono_indicators, + "call", EV_CALL_ACTIVE); + telephony_update_indicator(ofono_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + if (vc->status == CALL_STATUS_INCOMING) + telephony_calling_stopped_ind(); + vc->status = CALL_STATUS_ACTIVE; + } else if (g_str_equal(state, "alerting")) { + telephony_update_indicator(ofono_indicators, + "callsetup", EV_CALLSETUP_ALERTING); + vc->status = CALL_STATUS_ALERTING; + vc->originating = TRUE; + } else if (g_str_equal(state, "incoming")) { + /* state change from waiting to incoming */ + telephony_update_indicator(ofono_indicators, + "callsetup", EV_CALLSETUP_INCOMING); + telephony_incoming_call_ind(vc->number, + NUMBER_TYPE_TELEPHONY); + vc->status = CALL_STATUS_INCOMING; + vc->originating = FALSE; + } else if (g_str_equal(state, "held")) { + vc->status = CALL_STATUS_HELD; + if (find_vc_without_status(CALL_STATUS_HELD)) + telephony_update_indicator(ofono_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + else + telephony_update_indicator(ofono_indicators, + "callheld", + EV_CALLHELD_ON_HOLD); + } + } else if (g_str_equal(property, "Multiparty")) { + dbus_bool_t multiparty; + + dbus_message_iter_get_basic(&sub, &multiparty); + DBG("Multiparty %s", multiparty ? "True" : "False"); + vc->conference = multiparty; + } + + return TRUE; +} + +static struct voice_call *call_new(const char *path, DBusMessageIter *properties) +{ + struct voice_call *vc; + + DBG("%s", path); + + vc = g_new0(struct voice_call, 1); + vc->obj_path = g_strdup(path); + vc->watch = g_dbus_add_signal_watch(connection, NULL, path, + OFONO_VC_INTERFACE, "PropertyChanged", + handle_vc_property_changed, vc, NULL); + + while (dbus_message_iter_get_arg_type(properties) + == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter entry, value; + const char *property, *cli, *state; + dbus_bool_t multiparty; + + dbus_message_iter_recurse(properties, &entry); + dbus_message_iter_get_basic(&entry, &property); + + dbus_message_iter_next(&entry); + dbus_message_iter_recurse(&entry, &value); + + if (g_str_equal(property, "LineIdentification")) { + dbus_message_iter_get_basic(&value, &cli); + DBG("cli %s", cli); + vc->number = g_strdup(cli); + } else if (g_str_equal(property, "State")) { + dbus_message_iter_get_basic(&value, &state); + DBG("state %s", state); + if (g_str_equal(state, "incoming")) + vc->status = CALL_STATUS_INCOMING; + else if (g_str_equal(state, "dialing")) + vc->status = CALL_STATUS_DIALING; + else if (g_str_equal(state, "alerting")) + vc->status = CALL_STATUS_ALERTING; + else if (g_str_equal(state, "waiting")) + vc->status = CALL_STATUS_WAITING; + else if (g_str_equal(state, "held")) + vc->status = CALL_STATUS_HELD; + } else if (g_str_equal(property, "Multiparty")) { + dbus_message_iter_get_basic(&value, &multiparty); + DBG("Multipary %s", multiparty ? "True" : "False"); + vc->conference = multiparty; + } + + dbus_message_iter_next(properties); + } + + switch (vc->status) { + case CALL_STATUS_INCOMING: + DBG("CALL_STATUS_INCOMING"); + vc->originating = FALSE; + telephony_update_indicator(ofono_indicators, "callsetup", + EV_CALLSETUP_INCOMING); + telephony_incoming_call_ind(vc->number, NUMBER_TYPE_TELEPHONY); + break; + case CALL_STATUS_DIALING: + DBG("CALL_STATUS_DIALING"); + vc->originating = TRUE; + g_free(last_dialed_number); + last_dialed_number = g_strdup(vc->number); + telephony_update_indicator(ofono_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); + break; + case CALL_STATUS_ALERTING: + DBG("CALL_STATUS_ALERTING"); + vc->originating = TRUE; + g_free(last_dialed_number); + last_dialed_number = g_strdup(vc->number); + telephony_update_indicator(ofono_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + break; + case CALL_STATUS_WAITING: + DBG("CALL_STATUS_WAITING"); + vc->originating = FALSE; + telephony_update_indicator(ofono_indicators, "callsetup", + EV_CALLSETUP_INCOMING); + telephony_call_waiting_ind(vc->number, NUMBER_TYPE_TELEPHONY); + break; + } + + return vc; +} + +static void remove_pending(DBusPendingCall *call) +{ + pending = g_slist_remove(pending, call); + dbus_pending_call_unref(call); +} + +static void call_added(const char *path, DBusMessageIter *properties) +{ + struct voice_call *vc; + + DBG("%s", path); + + vc = find_vc(path); + if (vc) + return; + + vc = call_new(path, properties); + calls = g_slist_prepend(calls, vc); +} + +static void get_calls_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, entry; + + DBG(""); + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("ofono replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature"); + goto done; + } + + dbus_message_iter_recurse(&iter, &entry); + + while (dbus_message_iter_get_arg_type(&entry) + == DBUS_TYPE_STRUCT) { + const char *path; + DBusMessageIter value, properties; + + dbus_message_iter_recurse(&entry, &value); + dbus_message_iter_get_basic(&value, &path); + + dbus_message_iter_next(&value); + dbus_message_iter_recurse(&value, &properties); + + call_added(path, &properties); + + dbus_message_iter_next(&entry); + } + +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static void handle_network_property(const char *property, DBusMessageIter *variant) +{ + const char *status, *operator; + unsigned int signals_bar; + + if (g_str_equal(property, "Status")) { + dbus_message_iter_get_basic(variant, &status); + DBG("Status is %s", status); + if (g_str_equal(status, "registered")) { + net.status = NETWORK_REG_STATUS_HOME; + telephony_update_indicator(ofono_indicators, + "roam", EV_ROAM_INACTIVE); + telephony_update_indicator(ofono_indicators, + "service", EV_SERVICE_PRESENT); + } else if (g_str_equal(status, "roaming")) { + net.status = NETWORK_REG_STATUS_ROAM; + telephony_update_indicator(ofono_indicators, + "roam", EV_ROAM_ACTIVE); + telephony_update_indicator(ofono_indicators, + "service", EV_SERVICE_PRESENT); + } else { + net.status = NETWORK_REG_STATUS_NOSERV; + telephony_update_indicator(ofono_indicators, + "roam", EV_ROAM_INACTIVE); + telephony_update_indicator(ofono_indicators, + "service", EV_SERVICE_NONE); + } + } else if (g_str_equal(property, "Name")) { + dbus_message_iter_get_basic(variant, &operator); + DBG("Operator is %s", operator); + g_free(net.operator_name); + net.operator_name = g_strdup(operator); + } else if (g_str_equal(property, "SignalStrength")) { + dbus_message_iter_get_basic(variant, &signals_bar); + DBG("SignalStrength is %d", signals_bar); + net.signals_bar = signals_bar; + telephony_update_indicator(ofono_indicators, "signal", + (signals_bar + 20) / 21); + } +} + +static int parse_network_properties(DBusMessageIter *properties) +{ + int i; + + /* Reset indicators */ + for (i = 0; ofono_indicators[i].desc != NULL; i++) { + if (g_str_equal(ofono_indicators[i].desc, "battchg")) + ofono_indicators[i].val = 5; + else + ofono_indicators[i].val = 0; + } + + while (dbus_message_iter_get_arg_type(properties) + == DBUS_TYPE_DICT_ENTRY) { + const char *key; + DBusMessageIter value, entry; + + dbus_message_iter_recurse(properties, &entry); + dbus_message_iter_get_basic(&entry, &key); + + dbus_message_iter_next(&entry); + dbus_message_iter_recurse(&entry, &value); + + handle_network_property(key, &value); + + dbus_message_iter_next(properties); + } + + return 0; +} + +static void get_properties_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, properties; + int ret = 0; + + DBG(""); + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("ofono replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature"); + goto done; + } + + dbus_message_iter_recurse(&iter, &properties); + + ret = parse_network_properties(&properties); + if (ret < 0) { + error("Unable to parse %s.GetProperty reply", + OFONO_NETWORKREG_INTERFACE); + goto done; + } + + ret = send_method_call(OFONO_BUS_NAME, modem_obj_path, + OFONO_VCMANAGER_INTERFACE, "GetCalls", + get_calls_reply, NULL, DBUS_TYPE_INVALID); + if (ret < 0) + error("Unable to send %s.GetCalls", + OFONO_VCMANAGER_INTERFACE); + +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static void network_found(const char *path) +{ + int ret; + + DBG("%s", path); + + modem_obj_path = g_strdup(path); + + ret = send_method_call(OFONO_BUS_NAME, path, + OFONO_NETWORKREG_INTERFACE, "GetProperties", + get_properties_reply, NULL, DBUS_TYPE_INVALID); + if (ret < 0) + error("Unable to send %s.GetProperties", + OFONO_NETWORKREG_INTERFACE); +} + +static void modem_removed(const char *path) +{ + if (g_strcmp0(modem_obj_path, path) != 0) + return; + + DBG("%s", path); + + g_slist_free_full(calls, call_free); + calls = NULL; + + g_free(net.operator_name); + net.operator_name = NULL; + net.status = NETWORK_REG_STATUS_NOSERV; + net.signals_bar = 0; + + g_free(modem_obj_path); + modem_obj_path = NULL; +} + +static void parse_modem_interfaces(const char *path, DBusMessageIter *ifaces) +{ + DBG("%s", path); + + while (dbus_message_iter_get_arg_type(ifaces) == DBUS_TYPE_STRING) { + const char *iface; + + dbus_message_iter_get_basic(ifaces, &iface); + + if (g_str_equal(iface, OFONO_NETWORKREG_INTERFACE)) { + network_found(path); + return; + } + + dbus_message_iter_next(ifaces); + } + + modem_removed(path); +} + +static void modem_added(const char *path, DBusMessageIter *properties) +{ + if (modem_obj_path != NULL) { + DBG("Ignoring, modem already exist"); + return; + } + + DBG("%s", path); + + while (dbus_message_iter_get_arg_type(properties) + == DBUS_TYPE_DICT_ENTRY) { + const char *key; + DBusMessageIter interfaces, value, entry; + + dbus_message_iter_recurse(properties, &entry); + dbus_message_iter_get_basic(&entry, &key); + + dbus_message_iter_next(&entry); + dbus_message_iter_recurse(&entry, &value); + + if (strcasecmp(key, "Interfaces") != 0) + goto next; + + if (dbus_message_iter_get_arg_type(&value) + != DBUS_TYPE_ARRAY) { + error("Invalid Signature"); + return; + } + + dbus_message_iter_recurse(&value, &interfaces); + + parse_modem_interfaces(path, &interfaces); + + if (modem_obj_path != NULL) + return; + + next: + dbus_message_iter_next(properties); + } +} + +static void get_modems_reply(DBusPendingCall *call, void *user_data) +{ + DBusError err; + DBusMessage *reply; + DBusMessageIter iter, entry; + + DBG(""); + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("ofono replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + /* Skip modem selection if a modem already exist */ + if (modem_obj_path != NULL) + goto done; + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature"); + goto done; + } + + dbus_message_iter_recurse(&iter, &entry); + + while (dbus_message_iter_get_arg_type(&entry) + == DBUS_TYPE_STRUCT) { + const char *path; + DBusMessageIter item, properties; + + dbus_message_iter_recurse(&entry, &item); + dbus_message_iter_get_basic(&item, &path); + + dbus_message_iter_next(&item); + dbus_message_iter_recurse(&item, &properties); + + modem_added(path, &properties); + if (modem_obj_path != NULL) + break; + + dbus_message_iter_next(&entry); + } + +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static gboolean handle_network_property_changed(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + DBusMessageIter iter, variant; + const char *property; + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) { + error("Unexpected signature in networkregistration" + " PropertyChanged signal"); + return TRUE; + } + dbus_message_iter_get_basic(&iter, &property); + DBG("in handle_registration_property_changed()," + " the property is %s", property); + + dbus_message_iter_next(&iter); + dbus_message_iter_recurse(&iter, &variant); + + handle_network_property(property, &variant); + + return TRUE; +} + +static void handle_modem_property(const char *path, const char *property, + DBusMessageIter *variant) +{ + DBG("%s", property); + + if (g_str_equal(property, "Interfaces")) { + DBusMessageIter interfaces; + + if (dbus_message_iter_get_arg_type(variant) + != DBUS_TYPE_ARRAY) { + error("Invalid signature"); + return; + } + + dbus_message_iter_recurse(variant, &interfaces); + parse_modem_interfaces(path, &interfaces); + } +} + +static gboolean handle_modem_property_changed(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + DBusMessageIter iter, variant; + const char *property, *path; + + path = dbus_message_get_path(msg); + + /* Ignore if modem already exist and paths doesn't match */ + if (modem_obj_path != NULL && + g_str_equal(path, modem_obj_path) == FALSE) + return TRUE; + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) { + error("Unexpected signature in %s.%s PropertyChanged signal", + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + return TRUE; + } + + dbus_message_iter_get_basic(&iter, &property); + + dbus_message_iter_next(&iter); + dbus_message_iter_recurse(&iter, &variant); + + handle_modem_property(path, property, &variant); + + return TRUE; +} + +static gboolean handle_vcmanager_call_added(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + DBusMessageIter iter, properties; + const char *path = dbus_message_get_path(msg); + + /* Ignore call if modem path doesn't math */ + if (g_strcmp0(modem_obj_path, path) != 0) + return TRUE; + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) + != DBUS_TYPE_OBJECT_PATH) { + error("Unexpected signature in %s.%s signal", + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + return TRUE; + } + + dbus_message_iter_get_basic(&iter, &path); + dbus_message_iter_next(&iter); + dbus_message_iter_recurse(&iter, &properties); + + call_added(path, &properties); + + return TRUE; +} + +static void call_removed(const char *path) +{ + struct voice_call *vc; + + DBG("%s", path); + + vc = find_vc(path); + if (vc == NULL) + return; + + calls = g_slist_remove(calls, vc); + call_free(vc); +} + +static gboolean handle_vcmanager_call_removed(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + const char *path = dbus_message_get_path(msg); + + /* Ignore call if modem path doesn't math */ + if (g_strcmp0(modem_obj_path, path) != 0) + return TRUE; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) { + error("Unexpected signature in %s.%s signal", + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + return TRUE; + } + + call_removed(path); + + return TRUE; +} + +static gboolean handle_manager_modem_added(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + DBusMessageIter iter, properties; + const char *path; + + if (modem_obj_path != NULL) + return TRUE; + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) + != DBUS_TYPE_OBJECT_PATH) { + error("Unexpected signature in %s.%s signal", + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + return TRUE; + } + + dbus_message_iter_get_basic(&iter, &path); + dbus_message_iter_next(&iter); + dbus_message_iter_recurse(&iter, &properties); + + modem_added(path, &properties); + + return TRUE; +} + +static gboolean handle_manager_modem_removed(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + const char *path; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) { + error("Unexpected signature in %s.%s signal", + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + return TRUE; + } + + modem_removed(path); + + return TRUE; +} + +static void hal_battery_level_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply; + DBusError err; + dbus_int32_t level; + int *value = user_data; + + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + if (dbus_set_error_from_message(&err, reply)) { + error("hald replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_error_init(&err); + if (dbus_message_get_args(reply, &err, + DBUS_TYPE_INT32, &level, + DBUS_TYPE_INVALID) == FALSE) { + error("Unable to parse GetPropertyInteger reply: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + *value = (int) level; + + if (value == &battchg_last) + DBG("telephony-ofono: battery.charge_level.last_full" + " is %d", *value); + else if (value == &battchg_design) + DBG("telephony-ofono: battery.charge_level.design" + " is %d", *value); + else + DBG("telephony-ofono: battery.charge_level.current" + " is %d", *value); + + if ((battchg_design > 0 || battchg_last > 0) && battchg_cur >= 0) { + int new, max; + + if (battchg_last > 0) + max = battchg_last; + else + max = battchg_design; + + new = battchg_cur * 5 / max; + + telephony_update_indicator(ofono_indicators, "battchg", new); + } +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static void hal_get_integer(const char *path, const char *key, void *user_data) +{ + send_method_call("org.freedesktop.Hal", path, + "org.freedesktop.Hal.Device", + "GetPropertyInteger", + hal_battery_level_reply, user_data, + DBUS_TYPE_STRING, &key, + DBUS_TYPE_INVALID); +} + +static gboolean handle_hal_property_modified(DBusConnection *conn, + DBusMessage *msg, void *data) +{ + const char *path; + DBusMessageIter iter, array; + dbus_int32_t num_changes; + + path = dbus_message_get_path(msg); + + dbus_message_iter_init(msg, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INT32) { + error("Unexpected signature in hal PropertyModified signal"); + return TRUE; + } + + dbus_message_iter_get_basic(&iter, &num_changes); + dbus_message_iter_next(&iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in hal PropertyModified signal"); + return TRUE; + } + + dbus_message_iter_recurse(&iter, &array); + + while (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_INVALID) { + DBusMessageIter prop; + const char *name; + dbus_bool_t added, removed; + + dbus_message_iter_recurse(&array, &prop); + + if (!iter_get_basic_args(&prop, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_BOOLEAN, &added, + DBUS_TYPE_BOOLEAN, &removed, + DBUS_TYPE_INVALID)) { + error("Invalid hal PropertyModified parameters"); + break; + } + + if (g_str_equal(name, "battery.charge_level.last_full")) + hal_get_integer(path, name, &battchg_last); + else if (g_str_equal(name, "battery.charge_level.current")) + hal_get_integer(path, name, &battchg_cur); + else if (g_str_equal(name, "battery.charge_level.design")) + hal_get_integer(path, name, &battchg_design); + + dbus_message_iter_next(&array); + } + + return TRUE; +} + +static void add_watch(const char *sender, const char *path, + const char *interface, const char *member, + GDBusSignalFunction function) +{ + guint watch; + + watch = g_dbus_add_signal_watch(connection, sender, path, interface, + member, function, NULL, NULL); + + watches = g_slist_prepend(watches, GUINT_TO_POINTER(watch)); +} + +static void hal_find_device_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply; + DBusError err; + DBusMessageIter iter, sub; + int type; + const char *path; + + DBG("begin of hal_find_device_reply()"); + reply = dbus_pending_call_steal_reply(call); + + dbus_error_init(&err); + + if (dbus_set_error_from_message(&err, reply)) { + error("hald replied with an error: %s, %s", + err.name, err.message); + dbus_error_free(&err); + goto done; + } + + dbus_message_iter_init(reply, &iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + error("Unexpected signature in hal_find_device_reply()"); + goto done; + } + + dbus_message_iter_recurse(&iter, &sub); + + type = dbus_message_iter_get_arg_type(&sub); + + if (type != DBUS_TYPE_OBJECT_PATH && type != DBUS_TYPE_STRING) { + error("No hal device with battery capability found"); + goto done; + } + + dbus_message_iter_get_basic(&sub, &path); + + DBG("telephony-ofono: found battery device at %s", path); + + add_watch(NULL, path, "org.freedesktop.Hal.Device", + "PropertyModified", handle_hal_property_modified); + + hal_get_integer(path, "battery.charge_level.last_full", &battchg_last); + hal_get_integer(path, "battery.charge_level.current", &battchg_cur); + hal_get_integer(path, "battery.charge_level.design", &battchg_design); +done: + dbus_message_unref(reply); + remove_pending(call); +} + +static void handle_service_connect(DBusConnection *conn, void *user_data) +{ + DBG("telephony-ofono: %s found", OFONO_BUS_NAME); + + send_method_call(OFONO_BUS_NAME, OFONO_PATH, + OFONO_MANAGER_INTERFACE, "GetModems", + get_modems_reply, NULL, DBUS_TYPE_INVALID); +} + +static void handle_service_disconnect(DBusConnection *conn, void *user_data) +{ + DBG("telephony-ofono: %s exitted", OFONO_BUS_NAME); + + if (modem_obj_path) + modem_removed(modem_obj_path); +} + +int telephony_init(void) +{ + uint32_t features = AG_FEATURE_EC_ANDOR_NR | + AG_FEATURE_INBAND_RINGTONE | + AG_FEATURE_REJECT_A_CALL | + AG_FEATURE_ENHANCED_CALL_STATUS | + AG_FEATURE_ENHANCED_CALL_CONTROL | + AG_FEATURE_EXTENDED_ERROR_RESULT_CODES | + AG_FEATURE_THREE_WAY_CALLING; + const char *battery_cap = "battery"; + int ret; + guint watch; + + connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + + add_watch(OFONO_BUS_NAME, NULL, OFONO_MODEM_INTERFACE, + "PropertyChanged", handle_modem_property_changed); + add_watch(OFONO_BUS_NAME, NULL, OFONO_NETWORKREG_INTERFACE, + "PropertyChanged", handle_network_property_changed); + add_watch(OFONO_BUS_NAME, NULL, OFONO_MANAGER_INTERFACE, + "ModemAdded", handle_manager_modem_added); + add_watch(OFONO_BUS_NAME, NULL, OFONO_MANAGER_INTERFACE, + "ModemRemoved", handle_manager_modem_removed); + add_watch(OFONO_BUS_NAME, NULL, OFONO_VCMANAGER_INTERFACE, + "CallAdded", handle_vcmanager_call_added); + add_watch(OFONO_BUS_NAME, NULL, OFONO_VCMANAGER_INTERFACE, + "CallRemoved", handle_vcmanager_call_removed); + + watch = g_dbus_add_service_watch(connection, OFONO_BUS_NAME, + handle_service_connect, + handle_service_disconnect, + NULL, NULL); + if (watch == 0) + return -ENOMEM; + + watches = g_slist_prepend(watches, GUINT_TO_POINTER(watch)); + + ret = send_method_call("org.freedesktop.Hal", + "/org/freedesktop/Hal/Manager", + "org.freedesktop.Hal.Manager", + "FindDeviceByCapability", + hal_find_device_reply, NULL, + DBUS_TYPE_STRING, &battery_cap, + DBUS_TYPE_INVALID); + if (ret < 0) + return ret; + + DBG("telephony_init() successfully"); + + telephony_ready_ind(features, ofono_indicators, BTRH_NOT_SUPPORTED, + chld_str); + + return ret; +} + +static void remove_watch(gpointer data) +{ + g_dbus_remove_watch(connection, GPOINTER_TO_UINT(data)); +} + +static void pending_free(void *data) +{ + DBusPendingCall *call = data; + + if (!dbus_pending_call_get_completed(call)) + dbus_pending_call_cancel(call); + + dbus_pending_call_unref(call); +} + +void telephony_exit(void) +{ + DBG(""); + + g_free(last_dialed_number); + last_dialed_number = NULL; + + if (modem_obj_path) + modem_removed(modem_obj_path); + + g_slist_free_full(watches, remove_watch); + watches = NULL; + + g_slist_free_full(pending, pending_free); + pending = NULL; + + dbus_connection_unref(connection); + connection = NULL; + + telephony_deinit(); +} diff --git a/audio/telephony-tizen.c b/audio/telephony-tizen.c new file mode 100644 index 0000000..7b6bc16 --- /dev/null +++ b/audio/telephony-tizen.c @@ -0,0 +1,2616 @@ +/* + * Bluetooth protocol stack for Linux + * + * Copyright (c) 2000 - 2010 Samsung Electronics Co., Ltd. + * + * Contact: Chethan T N + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation; either version 2.1 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + *//*:Associate with "Bluetooth" */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "log.h" +#include "telephony.h" +#include "error.h" + + +/* CSD CALL plugin D-Bus definitions */ +#define CSD_CALL_BUS_NAME "org.tizen.csd" +#define CSD_CALL_INTERFACE "org.tizen.csd.Call" +#define CSD_CALL_INSTANCE "org.tizen.csd.Call.Instance" +#define CSD_CALL_CONFERENCE_PATH "/org/tizen/csd/call/conference" +#define CSD_DEVICE_INTERFACE "org.tizen.device" + +/* Phonebook definitions */ +#define PHONEBOOK_BUS_NAME "org.bluez.pb_agent" +#define PHONEBOOK_PATH "/org/bluez/pb_agent" +#define PHONEBOOK_INTERFACE "org.bluez.PbAgent.At" + +#define CALL_FLAG_NONE 0 +#define CALL_FLAG_PRESENTATION_ALLOWED 0x01 +#define CALL_FLAG_PRESENTATION_RESTRICTED 0x02 + +#define TELEPHONY_CSD_INTERFACE "org.tizen.telephony.csd" +#define TELEPHONY_CSD_OBJECT_PATH "/org/tizen/csd" + +#define HFP_AGENT_SERVICE "org.bluez.hfp_agent" +#define HFP_AGENT_PATH "/org/bluez/hfp_agent" +#define HFP_AGENT_INTERFACE "Org.Hfp.Bluez.Interface" + +/* Call status values as exported by the CSD CALL plugin */ +#define CSD_CALL_STATUS_IDLE 0 +#define CSD_CALL_STATUS_CREATE 1 +#define CSD_CALL_STATUS_COMING 2 +#define CSD_CALL_STATUS_PROCEEDING 3 +#define CSD_CALL_STATUS_MO_ALERTING 4 +#define CSD_CALL_STATUS_MT_ALERTING 5 +#define CSD_CALL_STATUS_WAITING 6 +#define CSD_CALL_STATUS_ANSWERED 7 +#define CSD_CALL_STATUS_ACTIVE 8 +#define CSD_CALL_STATUS_MO_RELEASE 9 +#define CSD_CALL_STATUS_MT_RELEASE 10 +#define CSD_CALL_STATUS_HOLD_INITIATED 11 +#define CSD_CALL_STATUS_HOLD 12 +#define CSD_CALL_STATUS_RETRIEVE_INITIATED 13 +#define CSD_CALL_STATUS_RECONNECT_PENDING 14 +#define CSD_CALL_STATUS_TERMINATED 15 +#define CSD_CALL_STATUS_SWAP_INITIATED 16 + + +#define PREFFERED_MESSAGE_STORAGE_LIST "(\"ME\",\"MT\",\"SM\",\"SR\"),(\"ME\",\"MT\",\"SM\",\"SR\"),(\"ME\",\"MT\",\"SM\",\"SR\")" + +#define PREFFERED_MESSAGE_STORAGE_MAX 500 +#define CALL_LOG_COUNT_MAX 30 +#define PHONEBOOK_COUNT_MAX 1000 + +#define PHONEBOOK_NAME_MAX_LENGTH 20 +#define PHONEBOOK_NUMBER_MAX_LENGTH 20 +#define PHONEBOOK_MAX_CHARACTER_LENGTH 20 + +#define PHONEBOOK_READ_RESP_LENGTH 20 + +enum { + CHARSET_UTF_8 = 0, + CHARSET_IRA +}; + +static const char *character_set_list[] = { + "\"UTF-8\"", "\"IRA\"" +}; + +static const char *phonebook_store_list[] = { + "\"ME\"", "\"DC\"", "\"MC\"", "\"RC\"" +}; + +#define CHARACTER_SET_LIST_SIZE (sizeof(character_set_list)/sizeof(const char *)) +#define PHONEBOOK_STORE_LIST_SIZE (sizeof(phonebook_store_list)/sizeof(const char *)) + +typedef struct { + uint8_t utf_8; + uint8_t gsm; +} GsmUnicodeTable; + + + /*0xC3 charcterset*/ +const GsmUnicodeTable gsm_unicode_C3[] = { + {0xA8,0x04}, {0xA9,0x05}, {0xB9,0x06}, {0xAC,0x07}, {0xB2,0x08}, + {0xB7,0x09}, {0x98,0x0B}, {0xB8,0x0C}, {0x85,0x0E}, {0xA5,0x0F}, + {0x86,0x1C}, {0xA6,0x1D}, {0x9F,0x1E}, {0x89,0x1F}, {0x84,0x5B}, + {0x96,0x5C}, {0x91,0x5D}, {0x9C,0x5E}, {0x80,0x5F}, {0xA4,0x7B}, + {0xB6,0x7C}, {0xB1,0x7D}, {0xBC,0x7E}, {0xA0,0x7F}, +}; + +/*0xCE charcterset*/ +const GsmUnicodeTable gsm_unicode_CE[] = { + {0x85,0x14}, {0xA1,0x50}, {0x98,0x19}, {0xA0,0x16}, {0x94,0x10}, + {0xA6,0x12}, {0x93,0x13}, {0x9E,0x1A}, {0x9B,0x14}, {0xA8,0x17}, + {0xA9,0x15}, +}; + +#define GSM_UNI_MAX_C3 (sizeof(gsm_unicode_C3)/sizeof(GsmUnicodeTable)) +#define GSM_UNI_MAX_CE (sizeof(gsm_unicode_CE)/sizeof(GsmUnicodeTable)) + + +static DBusConnection *ag_connection = NULL; + +static GSList *calls = NULL; + +static gboolean events_enabled = FALSE; +static uint32_t callerid = 0; + +/* Reference count for determining the call indicator status */ +static GSList *active_calls = NULL; +static GSList *sender_paths = NULL; + +typedef struct { + gchar *sender; + gchar *path; + unsigned int watch_id; +} sender_path_t; + +static struct indicator telephony_ag_indicators[] = +{ + { "battchg", "0-5", 5, TRUE }, + /* signal strength in terms of bars */ + { "signal", "0-5", 0, TRUE }, + { "service", "0,1", 0, TRUE }, + { "call", "0,1", 0, TRUE }, + { "callsetup", "0-3", 0, TRUE }, + { "callheld", "0-2", 0, FALSE }, + { "roam", "0,1", 0, TRUE }, + { NULL } +}; + +static char *call_status_str[] = { + "IDLE", + "CREATE", + "COMING", + "PROCEEDING", + "MO_ALERTING", + "MT_ALERTING", + "WAITING", + "ANSWERED", + "ACTIVE", + "MO_RELEASE", + "MT_RELEASE", + "HOLD_INITIATED", + "HOLD", + "RETRIEVE_INITIATED", + "RECONNECT_PENDING", + "TERMINATED", + "SWAP_INITIATED", + "???" +}; + +enum net_registration_status { + NETWORK_REG_STATUS_HOME, + NETWORK_REG_STATUS_ROAMING, + NETWORK_REG_STATUS_OFFLINE, + NETWORK_REG_STATUS_SEARCHING, + NETWORK_REG_STATUS_NO_SIM, + NETWORK_REG_STATUS_POWEROFF, + NETWORK_REG_STATUS_POWERSAFE, + NETWORK_REG_STATUS_NO_COVERAGE, + NETWORK_REG_STATUS_REJECTED, + NETWORK_REG_STATUS_UNKOWN +}; + +struct csd_call { + char *path; + int status; + gboolean originating; + gboolean emergency; + gboolean on_hold; + gboolean conference; + char *number; + gboolean setup; + uint32_t call_id; + char *sender; +}; + +static struct { + char *operator_name; + uint8_t status; + int32_t signal_bars; +} net = { + .operator_name = NULL, + .status = NETWORK_REG_STATUS_UNKOWN, + /* Init as 0 meaning inactive mode. In modem power off state + * can be be -1, but we treat all values as 0s regardless + * inactive or power off. */ + .signal_bars = 0, +}; + +/* Supported set of call hold operations */ +/*static const char *telephony_chld_str = "0,1,1x,2,2x,3,4";*/ +static const char *telephony_chld_str = "0,1,2,3"; + + +static char *subscriber_number = NULL; /* Subscriber number */ + + +static struct { + int32_t path_id; + int32_t charset_id; +} ag_pb_info = { + .path_id = 0, + .charset_id = 0, +}; + +static void call_set_status(struct csd_call *call, dbus_uint32_t status); + +static void free_sender_path(sender_path_t *s_path) +{ + if (s_path == NULL) + return; + + g_free(s_path->path); + g_free(s_path->sender); + g_free(s_path); +} + +static void free_sender_list() +{ + GSList *l; + + for (l = sender_paths; l != NULL; l = l->next) { + free_sender_path(l->data); + } + g_slist_free(sender_paths); + sender_paths = NULL; + return; +} + +static int telephony_remove_from_sender_list(const char *sender, const char *path) +{ + GSList *l; + sender_path_t *s_path; + + if (sender == NULL || path == NULL) + return -EINVAL; + + for (l = sender_paths; l != NULL; l = l->next) { + s_path = l->data; + if (s_path == NULL) + return -ENOENT; + if (g_strcmp0(s_path->path, path) == 0) { + g_dbus_remove_watch(ag_connection, s_path->watch_id); + sender_paths = g_slist_remove(sender_paths, s_path); + free_sender_path(s_path); + + /*Free sender_paths if no application is registered*/ + if (0 == g_slist_length(sender_paths)) { + g_slist_free(sender_paths); + sender_paths = NULL; + } + return 0; + } + } + return -ENOENT; +} + +static void remove_call_with_sender(gchar *sender) +{ + GSList *l; + + DBG("+\n"); + + if (sender == NULL) + return; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (g_strcmp0(call->sender, sender) == 0) + /* Release the Call and inform headset */ + call_set_status(call, CSD_CALL_STATUS_MT_RELEASE); + } + + DBG("-\n"); +} + +static void telephony_app_exit_cb(DBusConnection *conn, void *user_data) +{ + sender_path_t *s_path = (sender_path_t *)user_data; + + DBG("+\n"); + + if (s_path == NULL) + return; + + /* check any active call from application */ + remove_call_with_sender(s_path->sender); + + if (!telephony_remove_from_sender_list(s_path->sender, s_path->path)) + DBG("Application removed \n"); + else + DBG("Application not removed \n"); + DBG("-\n"); +} + +static gboolean telephony_is_registered(const char *path) +{ + GSList *l; + sender_path_t *s_path; + + if (path == NULL || sender_paths == NULL) + return FALSE; + + for (l = sender_paths; l != NULL; l = l->next) { + s_path = l->data; + if (s_path == NULL) + break; + + if (g_strcmp0(s_path->path, path) == 0) { + return TRUE; + } + } + return FALSE; +} + +static gboolean telephony_is_call_allowed(const char *path) +{ + GSList *l; + + if (path == NULL) + return FALSE; + + /*if call list doesn't exist the call should be allowed, since its a new call*/ + if (!calls) + return TRUE; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (g_strcmp0(call->path, path) == 0) + return TRUE; + + } + return FALSE; +} + +static int telephony_add_to_sender_list(const char *sender, const char *path) +{ + sender_path_t *s_path; + + if (sender == NULL || path == NULL) + return -EINVAL; + + /*check if already registered*/ + if (telephony_is_registered(path)) { + return -EEXIST; + } + + s_path = g_new0(sender_path_t, 1); + s_path->path = g_strdup(path); + s_path->sender = g_strdup(sender); + s_path->watch_id = g_dbus_add_disconnect_watch(ag_connection, sender, + telephony_app_exit_cb, s_path, NULL); + sender_paths = g_slist_append(sender_paths, s_path); + return 0; +} + +static struct csd_call *find_call_with_id(uint32_t call_id) + +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->call_id == call_id) + return call; + } + + return NULL; +} + +static struct csd_call *find_non_held_call(void) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == CSD_CALL_STATUS_IDLE) + continue; + + if (call->status != CSD_CALL_STATUS_HOLD) + return call; + } + + return NULL; +} + +static struct csd_call *find_non_idle_call(void) +{ + GSList *l; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status != CSD_CALL_STATUS_IDLE) + return call; + } + + return NULL; +} + +static struct csd_call *find_call_with_status(int status) +{ + GSList *l; + + if (NULL != calls) { + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == status) + return call; + } + } + return NULL; +} + +static void csd_call_free(struct csd_call *call) +{ + if (!call) + return; + + g_free(call->path); + g_free(call->number); + g_free(call->sender); + + g_free(call); +} + +static int reject_call(struct csd_call *call) +{ + DBusMessage *msg; + + DBG("+\n"); + + DBG("telephony-tizen: reject_call "); + + msg = dbus_message_new_method_call(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "RejectCall"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + DBG(" Path =[ %s] and Call id = [%d]\n", call->path, call->call_id); + if (!dbus_message_append_args(msg, DBUS_TYPE_UINT32, &call->call_id, + DBUS_TYPE_STRING, &call->path, + DBUS_TYPE_STRING, &call->sender, + DBUS_TYPE_INVALID)) { + + DBG("dbus_message_append_args -ENOMEM\n"); + dbus_message_unref(msg); + return -ENOMEM; + } + + g_dbus_send_message(ag_connection, msg); + + DBG("-\n"); + + return 0; + +} + +static int release_call(struct csd_call *call) +{ + DBusMessage *msg; + + DBG("+\n"); + + DBG("telephony-tizen: release_call "); + + msg = dbus_message_new_method_call(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "ReleaseCall"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + DBG("Path =[ %s] and Call id = [%d]\n", call->path, call->call_id); + if (!dbus_message_append_args(msg, DBUS_TYPE_UINT32, &call->call_id, + DBUS_TYPE_STRING, &call->path, + DBUS_TYPE_STRING, &call->sender, + DBUS_TYPE_INVALID)) { + + DBG("dbus_message_append_args -ENOMEM\n"); + dbus_message_unref(msg); + return -ENOMEM; + } + + g_dbus_send_message(ag_connection, msg); + + DBG("-\n"); + + return 0; +} + +static int release_conference(void) +{ + GSList *l; + + DBG("+\n"); + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->conference) + release_call(call); + } + + DBG("-\n"); + + return 0; +} + +static int dbus_method_call_send(const char *dest, const char *path, + const char *interface, const char *method, + DBusPendingCallNotifyFunction cb, + void *user_data, int type, ...) +{ + DBusMessage *msg; + DBusPendingCall *call; + va_list args; + + DBG("+\n"); + + msg = dbus_message_new_method_call(dest, path, interface, method); + if (!msg) { + error("Unable to allocate new D-Bus %s message", method); + return -ENOMEM; + } + + va_start(args, type); + + if (!dbus_message_append_args_valist(msg, type, args)) { + dbus_message_unref(msg); + va_end(args); + return -EIO; + } + + va_end(args); + + if (!cb) { + g_dbus_send_message(ag_connection, msg); + return 0; + } + + if (!dbus_connection_send_with_reply(ag_connection, msg, &call, -1)) { + error("Sending %s failed", method); + dbus_message_unref(msg); + return -EIO; + } + + dbus_pending_call_set_notify(call, cb, user_data, NULL); + dbus_message_unref(msg); + DBG("-\n"); + + return 0; +} + +static int answer_call(struct csd_call *call) +{ + DBusMessage *msg; + DBG("+\n"); + + msg = dbus_message_new_method_call(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "AnswerCall"); + if (!msg) { + error("Unable to allocate new D-Bus message"); + return -ENOMEM; + } + + if (!dbus_message_append_args(msg, DBUS_TYPE_UINT32, &call->call_id, + DBUS_TYPE_STRING, &call->path, + DBUS_TYPE_STRING, &call->sender, + DBUS_TYPE_INVALID)) { + + DBG("dbus_message_append_args -ENOMEM\n"); + dbus_message_unref(msg); + return -ENOMEM; + } + g_dbus_send_message(ag_connection, msg); + DBG("-\n"); + return 0; +} + +static int reject_accept_call(struct csd_call *call, void *telephony_device) +{ + uint32_t chld_value = 1; + + return dbus_method_call_send(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "ThreewayCall", + NULL, telephony_device, + DBUS_TYPE_UINT32, &chld_value, + DBUS_TYPE_STRING, &call->path, + DBUS_TYPE_STRING, &call->sender, + DBUS_TYPE_INVALID); +} + +static int number_type(const char *number) +{ + if (number == NULL) + return NUMBER_TYPE_TELEPHONY; + + if (number[0] == '+' || strncmp(number, "00", 2) == 0) + return NUMBER_TYPE_INTERNATIONAL; + + return NUMBER_TYPE_TELEPHONY; +} + +/* Since we dont have support from voice call regarding call conference this */ +/* function will handle both join and split scenarios */ + +/* This function checks the status of each call in the list and set/unset */ +/* conference status based on below algorithm */ +/* If more that one active/held calls are there, conf flag of those calls will be set */ +/* If only one active/held call is there, conf flag of those calls will be unset */ +static void handle_conference(void) +{ + GSList *l; + struct csd_call *first_active_call = NULL; + struct csd_call *first_held_call = NULL; + int active_call_count = 0; + int held_call_count = 0; + + for (l = calls; l != NULL; l = l->next) { + struct csd_call *call = l->data; + + if (call->status == CSD_CALL_STATUS_ACTIVE) { + if (first_active_call == NULL) + first_active_call = call; + + active_call_count++; + + if (active_call_count >= 2) { + if (!first_active_call->conference) + first_active_call->conference = TRUE; + call->conference = TRUE; + } + + } else if (call->status == CSD_CALL_STATUS_HOLD) { + if (first_held_call == NULL) + first_held_call = call; + + held_call_count++; + + if (held_call_count >= 2) { + if (!first_held_call->conference) + first_held_call->conference = TRUE; + call->conference = TRUE; + } + } + } + + if (active_call_count == 1) { + if (first_active_call->conference) + first_active_call->conference = FALSE; + } + + if (held_call_count == 1) { + if (first_held_call->conference) + first_held_call->conference = FALSE; + } +} + +static void call_set_status(struct csd_call *call, dbus_uint32_t status) +{ + dbus_uint32_t prev_status; + int callheld = 0; + + DBG("+\n"); + + callheld = telephony_get_indicator(telephony_ag_indicators, "callheld"); + + prev_status = call->status; + DBG("Call %s Call id %d changed from %s to %s", call->path, call->call_id, + call_status_str[prev_status], call_status_str[status]); + + if (prev_status == status) { + DBG("Ignoring CSD Call state change to existing state"); + return; + } + + call->status = (int) status; + + switch (status) { + case CSD_CALL_STATUS_IDLE: + if (call->setup) { + telephony_update_indicator(telephony_ag_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + if (!call->originating) + telephony_calling_stopped_ind(); + } + + g_free(call->number); + call->number = NULL; + call->originating = FALSE; + call->emergency = FALSE; + call->on_hold = FALSE; + call->conference = FALSE; + call->setup = FALSE; + break; + case CSD_CALL_STATUS_CREATE: + call->originating = TRUE; + call->setup = TRUE; + break; + case CSD_CALL_STATUS_COMING: + call->originating = FALSE; + call->setup = TRUE; + break; + case CSD_CALL_STATUS_PROCEEDING: + break; + case CSD_CALL_STATUS_MO_ALERTING: + telephony_update_indicator(telephony_ag_indicators, "callsetup", + EV_CALLSETUP_ALERTING); + break; + case CSD_CALL_STATUS_MT_ALERTING: + /* Some headsets expect incoming call notification before they + * can send ATA command. When call changed status from waiting + * to alerting we need to send missing notification. Otherwise + * headsets like Nokia BH-108 or BackBeat 903 are unable to + * answer incoming call that was previously waiting. */ + if (prev_status == CSD_CALL_STATUS_WAITING) + telephony_incoming_call_ind(call->number, + number_type(call->number)); + break; + case CSD_CALL_STATUS_WAITING: + break; + case CSD_CALL_STATUS_ANSWERED: + break; + case CSD_CALL_STATUS_ACTIVE: + if (call->on_hold) { + call->on_hold = FALSE; + if (find_call_with_status(CSD_CALL_STATUS_HOLD)) + telephony_update_indicator(telephony_ag_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + else + telephony_update_indicator(telephony_ag_indicators, + "callheld", + EV_CALLHELD_NONE); + } else { + if (!g_slist_find(active_calls, call)) + active_calls = g_slist_prepend(active_calls, call); + if (g_slist_length(active_calls) == 1) + telephony_update_indicator(telephony_ag_indicators, + "call", + EV_CALL_ACTIVE); + /* Upgrade callheld status if necessary */ + if (callheld == EV_CALLHELD_ON_HOLD) + telephony_update_indicator(telephony_ag_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + telephony_update_indicator(telephony_ag_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + if (!call->originating) + telephony_calling_stopped_ind(); + call->setup = FALSE; + } + break; + case CSD_CALL_STATUS_MO_RELEASE: + case CSD_CALL_STATUS_MT_RELEASE: + active_calls = g_slist_remove(active_calls, call); + if (g_slist_length(active_calls) == 0) + telephony_update_indicator(telephony_ag_indicators, "call", + EV_CALL_INACTIVE); + + if ((prev_status == CSD_CALL_STATUS_MO_ALERTING) || + (prev_status == CSD_CALL_STATUS_COMING) || + (prev_status == CSD_CALL_STATUS_CREATE) || + (prev_status == CSD_CALL_STATUS_WAITING)) { + telephony_update_indicator(telephony_ag_indicators, + "callsetup", + EV_CALLSETUP_INACTIVE); + } + + if (prev_status == CSD_CALL_STATUS_COMING) { + if (!call->originating) + telephony_calling_stopped_ind(); + } + calls = g_slist_remove(calls, call); + csd_call_free(call); + + break; + case CSD_CALL_STATUS_HOLD_INITIATED: + break; + case CSD_CALL_STATUS_HOLD: + call->on_hold = TRUE; + if (find_non_held_call()) + telephony_update_indicator(telephony_ag_indicators, + "callheld", + EV_CALLHELD_MULTIPLE); + else + telephony_update_indicator(telephony_ag_indicators, + "callheld", + EV_CALLHELD_ON_HOLD); + break; + case CSD_CALL_STATUS_RETRIEVE_INITIATED: + break; + case CSD_CALL_STATUS_RECONNECT_PENDING: + break; + case CSD_CALL_STATUS_TERMINATED: + if (call->on_hold && + !find_call_with_status(CSD_CALL_STATUS_HOLD)) { + telephony_update_indicator(telephony_ag_indicators, + "callheld", + EV_CALLHELD_NONE); + return; + } + + if (callheld == EV_CALLHELD_MULTIPLE && + find_call_with_status(CSD_CALL_STATUS_HOLD) && + !find_call_with_status(CSD_CALL_STATUS_ACTIVE)) + telephony_update_indicator(telephony_ag_indicators, + "callheld", + EV_CALLHELD_ON_HOLD); + break; + case CSD_CALL_STATUS_SWAP_INITIATED: + break; + default: + error("Unknown call status %u", status); + break; + } + + /* Update the conference status of each call */ + handle_conference(); + + DBG("-\n"); +} + +static void telephony_chld_reply(DBusPendingCall *call, void *data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + DBusError derr; + + DBG("redial_reply"); + + dbus_error_init(&derr); + if (!dbus_set_error_from_message(&derr, reply)) { + DBG("chld reply: cmd is valid"); + telephony_dial_number_rsp(data, CME_ERROR_NONE); + goto done; + } + + DBG("chld_reply reply: %s", derr.message); + + dbus_error_free(&derr); + telephony_dial_number_rsp(data, CME_ERROR_AG_FAILURE); + +done: + dbus_message_unref(reply); +} + +void telephony_call_hold_req(void *telephony_device, const char *cmd) +{ + const char *idx; + struct csd_call *call; + int err = 0; + uint32_t chld_value; + GSList *l = NULL; + sender_path_t *s_path = NULL; + + DBG("+\n"); + + DBG("telephony-tizen: got call hold request %s", cmd); + + /* Find any Ongoing call, in active/held/waiting */ + if (NULL == (call = find_call_with_status(CSD_CALL_STATUS_ACTIVE))) + if (NULL == (call = find_call_with_status( + CSD_CALL_STATUS_HOLD))) + if (NULL == (call = find_call_with_status( + CSD_CALL_STATUS_WAITING))) { + DBG("No Onging Call \n"); + telephony_call_hold_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + /*Get sender path using call path*/ + for (l = sender_paths; l != NULL; l = l->next) { + s_path = l->data; + if (s_path == NULL) { + telephony_call_hold_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + if (g_strcmp0(s_path->path, call->path) == 0) + break; + } + + if (s_path == NULL) { + telephony_call_hold_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + idx = &cmd[0]; + chld_value = strtoul(idx, NULL, 0); + + DBG("Sender = %s path = %s \n", s_path->sender, s_path->path); + + err = dbus_method_call_send(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "ThreewayCall", + telephony_chld_reply, telephony_device, + DBUS_TYPE_UINT32, &chld_value, + DBUS_TYPE_STRING, &call->path, + DBUS_TYPE_STRING, &call->sender, + DBUS_TYPE_INVALID); + + if (err) + telephony_call_hold_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + DBG("-\n"); +} + +static int update_registration_status(uint8_t status) +{ + uint8_t new_status; + int ret = 0; + DBG("+\n"); + + new_status = status; + + if (net.status == new_status) + return ret; + + switch (new_status) { + case NETWORK_REG_STATUS_HOME: + ret = telephony_update_indicator(telephony_ag_indicators, "roam", + EV_ROAM_INACTIVE); + + if (net.status > NETWORK_REG_STATUS_ROAMING) { + ret = telephony_update_indicator(telephony_ag_indicators, + "service", + EV_SERVICE_PRESENT); + } + break; + case NETWORK_REG_STATUS_ROAMING: + ret = telephony_update_indicator(telephony_ag_indicators, "roam", + EV_ROAM_ACTIVE); + + if (net.status > NETWORK_REG_STATUS_ROAMING) { + ret = telephony_update_indicator(telephony_ag_indicators, + "service", + EV_SERVICE_PRESENT); + } + break; + case NETWORK_REG_STATUS_OFFLINE: + case NETWORK_REG_STATUS_SEARCHING: + case NETWORK_REG_STATUS_NO_SIM: + case NETWORK_REG_STATUS_POWEROFF: + case NETWORK_REG_STATUS_POWERSAFE: + case NETWORK_REG_STATUS_NO_COVERAGE: + case NETWORK_REG_STATUS_REJECTED: + case NETWORK_REG_STATUS_UNKOWN: + if (net.status < NETWORK_REG_STATUS_OFFLINE) { + ret = telephony_update_indicator(telephony_ag_indicators, + "service", + EV_SERVICE_NONE); + } + break; + } + + net.status = new_status; + + DBG("telephony-tizen: registration status changed: %d", status); + DBG("-\n"); + + return ret; +} + +static int update_signal_strength(int32_t signal_bars) +{ + DBG("+\n"); + + if (signal_bars < 0) { + DBG("signal strength smaller than expected: %d < 0", + signal_bars); + signal_bars = 0; + } else if (signal_bars > 5) { + DBG("signal strength greater than expected: %d > 5", + signal_bars); + signal_bars = 5; + } + + if (net.signal_bars == signal_bars) + return 0; + + net.signal_bars = signal_bars; + DBG("telephony-tizen: signal strength updated: %d/5", signal_bars); + + return telephony_update_indicator(telephony_ag_indicators, "signal", signal_bars); +} + +static int update_battery_strength(int32_t battery_level) +{ + int current_battchg = 0; + + DBG("+\n"); + + current_battchg = telephony_get_indicator(telephony_ag_indicators, "battchg"); + + if (battery_level < 0) { + DBG("Battery strength smaller than expected: %d < 0", + battery_level); + battery_level = 0; + } else if (battery_level > 5) { + DBG("Battery strength greater than expected: %d > 5", + battery_level); + battery_level = 5; + } + if (current_battchg == battery_level) + return 0; + + DBG("telephony-tizen: battery strength updated: %d/5", battery_level); + DBG("-\n"); + + return telephony_update_indicator(telephony_ag_indicators, + "battchg", battery_level); +} + + +static int update_operator_name(const char *name) +{ + DBG("+\n"); + if (name == NULL) + return -EINVAL; + + g_free(net.operator_name); + net.operator_name = g_strndup(name, 16); + DBG("telephony-tizen: operator name updated: %s", name); + DBG("-\n"); + return 0; +} + +static int update_subscriber_number(const char *number) +{ + DBG("+\n"); + if (number == NULL) + return -EINVAL; + + g_free(subscriber_number); + subscriber_number = g_strdup(number); + DBG("telephony-tizen: subscriber_number updated: %s", subscriber_number); + DBG("-\n"); + return 0; +} + +static DBusMessage *telephony_error_reply(DBusMessage *msg, int error) +{ + switch (error) { + case -ENOENT: + return btd_error_not_available(msg); + case -ENODEV: + return btd_error_not_connected(msg); + case -EBUSY: + return btd_error_busy(msg); + case -EINVAL: + return btd_error_invalid_args(msg); + case -EEXIST: + return btd_error_already_exists(msg); + case -ENOMEM: + return btd_error_failed(msg, "No memory"); + case -EIO: + return btd_error_failed(msg, "I/O error"); + default: + return dbus_message_new_method_return(msg); + } +} + +static struct csd_call *create_call(DBusMessage *msg, const char *path, + const char *number, uint32_t call_id, + const char *sender) + +{ + struct csd_call *call; + + call = find_call_with_id(call_id); + if (!call) { + call = g_new0(struct csd_call, 1); + call->path = g_strdup(path); + call->call_id = call_id; + call->number = g_strdup(number); + call->sender = g_strdup(sender); + calls = g_slist_append(calls, call); + } + return call; +} + +static DBusMessage *incoming(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + const char *number, *call_path; + const char *sender; + struct csd_call *call; + uint32_t call_id; + int ret; + DBG("+\n"); + DBG("telephony_incoming()\n"); + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_STRING, &call_path, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_UINT32, &call_id, + DBUS_TYPE_STRING, &sender, + DBUS_TYPE_INVALID)) { + + return btd_error_invalid_args(msg); + } + + ret = telephony_is_registered(call_path); + if (!ret) + return telephony_error_reply(msg, -ENOENT); + + /*Check in the active call list, if any of the call_path exists if not don't allow + the call since it is initated by other applicatoin*/ + + ret = telephony_is_call_allowed(call_path); + if (!ret) + return telephony_error_reply(msg, -ENOENT); + + + call = create_call(msg, call_path, number, call_id, sender); + + DBG("Incoming call to %s from number %s call id %d", call_path, number, call_id); + ret = telephony_update_indicator(telephony_ag_indicators, "callsetup", + EV_CALLSETUP_INCOMING); + if (ret) + return telephony_error_reply(msg, ret); + + if (find_call_with_status(CSD_CALL_STATUS_ACTIVE) || + find_call_with_status(CSD_CALL_STATUS_HOLD)) { + ret = telephony_call_waiting_ind(call->number, + number_type(call->number)); + if (ret) + return telephony_error_reply(msg, ret); + + call_set_status(call, CSD_CALL_STATUS_WAITING); + } else { + ret = telephony_incoming_call_ind(call->number, + number_type(call->number)); + if (ret) + return telephony_error_reply(msg, ret); + + call_set_status(call, CSD_CALL_STATUS_COMING); + } + DBG("-\n"); + return dbus_message_new_method_return(msg); +} + +static DBusMessage *outgoing(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + const char *number, *call_path; + const char *sender; + struct csd_call *call; + uint32_t call_id; + int ret; + + DBG("+\n"); + DBG("telephony_outgoing_call()\n"); + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_STRING, &call_path, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_UINT32, &call_id, + DBUS_TYPE_STRING, &sender, + DBUS_TYPE_INVALID)) { + return btd_error_invalid_args(msg); + } + + ret = telephony_is_registered(call_path); + if (!ret) + return telephony_error_reply(msg, -ENOENT); + + /*Check in the active call list, if any of the call_path exists if not don't allow + the call since it is initated by other applicatoin*/ + + ret = telephony_is_call_allowed(call_path); + if (!ret) + return telephony_error_reply(msg, -ENOENT); + + call = create_call(msg, call_path, number, call_id, sender); + + DBG("Outgoing call to %s from number %s call id %d", call_path, number, call_id); + + call_set_status(call, CSD_CALL_STATUS_CREATE); + + ret = telephony_update_indicator(telephony_ag_indicators, "callsetup", + EV_CALLSETUP_OUTGOING); + if (ret) + return telephony_error_reply(msg, ret); + + DBG("-\n"); + return dbus_message_new_method_return(msg); +} + +static DBusMessage *set_call_status(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct csd_call *call; + dbus_uint32_t status, call_id; + const char *call_path; + const char *sender; + int ret; + + DBG("+\n"); + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_STRING, &call_path, + DBUS_TYPE_UINT32, &status, + DBUS_TYPE_UINT32, &call_id, + DBUS_TYPE_STRING, &sender, + DBUS_TYPE_INVALID)) { + error("Unexpected paramters in Instance.CallStatus() signal"); + return btd_error_invalid_args(msg); + } + + if (status > 16) { + return btd_error_invalid_args(msg); + } + + ret = telephony_is_registered(call_path); + if (!ret) + return telephony_error_reply(msg, -ENOENT); + + ret = telephony_is_call_allowed(call_path); + if (!ret) + return telephony_error_reply(msg, -ENOENT); + + DBG("status = [%d] and call_id = [%d] \n", status, call_id); + + call = find_call_with_id(call_id); + if (!call) { +/* + call_path is equal to CSD_CALL_PATH then we should update the call list + since the call_path is sent from native AG applicaton + + Added for updation of the call status if the call is not added in the call list +*/ + call = create_call(msg, call_path, NULL, call_id, sender); + } + call_set_status(call, status); + DBG("-\n"); + return dbus_message_new_method_return(msg); +} + +static DBusMessage *register_telephony_agent(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + gboolean flag; + const char *sender; + const char *path; + int ret; + + DBG("+\n"); + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_BOOLEAN, &flag, + DBUS_TYPE_STRING, &path, + DBUS_TYPE_STRING, &sender, + DBUS_TYPE_INVALID)) { + error("Unexpected parameters in RegisterSenderPath"); + return btd_error_invalid_args(msg); + } + + DBG("flag = %d \n", flag); + DBG("Sender = %s \n", sender); + DBG("path = %s \n", path); + + if (flag) + ret = telephony_add_to_sender_list(sender, path); + else + ret = telephony_remove_from_sender_list(sender, path); + + if (ret) + return telephony_error_reply(msg, ret); + + DBG("-\n"); + + return dbus_message_new_method_return(msg); +} + +static DBusMessage *set_property(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + const char *property; + DBusMessageIter iter; + DBusMessageIter sub; + int ret; + + if (!dbus_message_iter_init(msg, &iter)) + return btd_error_invalid_args(msg); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&iter, &property); + dbus_message_iter_next(&iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) + return btd_error_invalid_args(msg); + dbus_message_iter_recurse(&iter, &sub); + + if (g_str_equal("RegistrationChanged", property)) { + uint8_t value; + if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_BYTE) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&sub, &value); + + ret = update_registration_status(value); + if (ret) + return telephony_error_reply(msg, ret); + } else if (g_str_equal("OperatorNameChanged", property)) { + const char *name; + if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_STRING) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&sub, &name); + + ret = update_operator_name(name); + if (ret) + return telephony_error_reply(msg, ret); + } else if (g_str_equal("SignalBarsChanged", property)) { + int32_t value; + if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INT32) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&sub, &value); + ret = update_signal_strength(value); + if (ret) + return telephony_error_reply(msg, ret); + + } else if (g_str_equal("BatteryBarsChanged", property)) { + int32_t value; + if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INT32) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&sub, &value); + ret = update_battery_strength(value); + if (ret) + return telephony_error_reply(msg, ret); + + } else if (g_str_equal("SubscriberNumberChanged", property)) { + const char *number; + if (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_STRING) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&sub, &number); + ret = update_subscriber_number(number); + if (ret) + return telephony_error_reply(msg, ret); + } + + return dbus_message_new_method_return(msg); +} + +static GDBusMethodTable telephony_methods[] = { + { GDBUS_METHOD("Incoming", + GDBUS_ARGS({ "path", "s" }, { "number", "s" }, + { "id", "u" }, { "sender", "s" }), + NULL, + incoming) }, + { GDBUS_METHOD("Outgoing", + GDBUS_ARGS({ "path", "s" }, { "number", "s" }, + { "id", "u" }, { "sender", "s" }), + NULL, + outgoing) }, + { GDBUS_METHOD("SetCallStatus", + GDBUS_ARGS({ "path", "s" }, { "status", "u" }, + { "id", "u" }, { "sender", "s" }), + NULL, + set_call_status) }, + { GDBUS_METHOD("RegisterTelephonyAgent", + GDBUS_ARGS({ "flag", "b" }, { "path", "s" }, + { "sender", "s" }), + NULL, + register_telephony_agent) }, + { GDBUS_METHOD("SetProperty", + GDBUS_ARGS({ "name", "s" }, { "property", "v" }), + NULL, + set_property) }, + { } +}; + +static void path_unregister(void *data) +{ + DBG("+\n"); + g_dbus_unregister_interface(ag_connection, TELEPHONY_CSD_OBJECT_PATH, + TELEPHONY_CSD_INTERFACE); + DBG("-\n"); +} + +/*API's that shall be ported*/ +int telephony_init(void) +{ + uint32_t features = AG_FEATURE_EC_ANDOR_NR | + AG_FEATURE_REJECT_A_CALL | + AG_FEATURE_ENHANCED_CALL_STATUS | + AG_FEATURE_THREE_WAY_CALLING | + AG_FEATURE_VOICE_RECOGNITION; + int i; + + DBG(""); + + ag_connection = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + + if (!g_dbus_register_interface(ag_connection, TELEPHONY_CSD_OBJECT_PATH, + TELEPHONY_CSD_INTERFACE, + telephony_methods, NULL, NULL, + NULL, path_unregister)) { + error("D-Bus failed to register %s interface", TELEPHONY_CSD_INTERFACE); + return -1; + } + + /* Reset indicators */ + for (i = 0; telephony_ag_indicators[i].desc != NULL; i++) { + if (g_str_equal(telephony_ag_indicators[i].desc, "battchg")) + telephony_ag_indicators[i].val = 5; + else + telephony_ag_indicators[i].val = 0; + } + + /*Initializatoin of the indicators*/ + telephony_ready_ind(features, telephony_ag_indicators, + BTRH_NOT_SUPPORTED, + telephony_chld_str); + + return 0; +} + +void telephony_exit(void) +{ + DBG(""); + + g_free(net.operator_name); + net.operator_name = NULL; + + g_free(subscriber_number); + subscriber_number = NULL; + + net.status = NETWORK_REG_STATUS_UNKOWN; + net.signal_bars = 0; + + g_slist_free(active_calls); + active_calls = NULL; + + g_slist_foreach(calls, (GFunc) csd_call_free, NULL); + g_slist_free(calls); + calls = NULL; + + free_sender_list(); + + g_dbus_unregister_interface(ag_connection, TELEPHONY_CSD_OBJECT_PATH, + TELEPHONY_CSD_INTERFACE); + + dbus_connection_unref(ag_connection); + ag_connection = NULL; + + telephony_deinit(); +} + +void telephony_device_connected(void *telephony_device) +{ + DBG("telephony-tizen: device %p connected", telephony_device); +} + +void telephony_device_disconnected(void *telephony_device) +{ + DBG("telephony-tizen: device %p disconnected", telephony_device); + events_enabled = FALSE; +} + +void telephony_event_reporting_req(void *telephony_device, int ind) +{ + events_enabled = ind == 1 ? TRUE : FALSE; + + telephony_event_reporting_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_response_and_hold_req(void *telephony_device, int rh) +{ + DBG("telephony-tizen: response_and_hold_req - device %p disconnected", + telephony_device); + + telephony_response_and_hold_rsp(telephony_device, + CME_ERROR_NOT_SUPPORTED); +} + +static void telephony_dial_number_reply(DBusPendingCall *call, void *data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + DBusError derr; + + DBG("redial_reply"); + + dbus_error_init(&derr); + if (!dbus_set_error_from_message(&derr, reply)) { + DBG("hfg reply: dial done successfully"); + telephony_dial_number_rsp(data, CME_ERROR_NONE); + goto done; + } + + DBG("dial_reply reply: %s", derr.message); + + dbus_error_free(&derr); + telephony_dial_number_rsp(data, CME_ERROR_AG_FAILURE); + +done: + dbus_message_unref(reply); +} + +void telephony_dial_number_req(void *telephony_device, const char *number) +{ + uint32_t flags = callerid; + + DBG("telephony-tizen: dial request to %s", number); + + if (strncmp(number, "*31#", 4) == 0) { + number += 4; + flags = CALL_FLAG_PRESENTATION_ALLOWED; + } else if (strncmp(number, "#31#", 4) == 0) { + number += 4; + flags = CALL_FLAG_PRESENTATION_RESTRICTED; + } else if (number[0] == '>') { + int location = strtol(&number[1], NULL, 0); + + if (0 != dbus_method_call_send(HFP_AGENT_SERVICE, + HFP_AGENT_PATH, HFP_AGENT_INTERFACE, + "DialMemory", + telephony_dial_number_reply, telephony_device, + DBUS_TYPE_INT32, &location, + DBUS_TYPE_INVALID)) { + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + } + return; + } + + if (0 != dbus_method_call_send(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "DialNum", + NULL, NULL, + DBUS_TYPE_STRING, &number, + DBUS_TYPE_UINT32, &flags, + DBUS_TYPE_INVALID)) { + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_dial_number_rsp(telephony_device, CME_ERROR_NONE); +} + + +void telephony_terminate_call_req(void *telephony_device) +{ + struct csd_call *call; + struct csd_call *alerting; + int err; + + DBG("+\n"); + + call = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + if (!call) + call = find_non_idle_call(); + + if (!call) { + error("No active call"); + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + if (NULL != find_call_with_status(CSD_CALL_STATUS_WAITING)) + err = reject_accept_call(call, telephony_device); + else if (NULL != (alerting = find_call_with_status(CSD_CALL_STATUS_CREATE))) + err = reject_call(alerting); + else if (NULL != (alerting = find_call_with_status(CSD_CALL_STATUS_MO_ALERTING))) + err = reject_call(alerting); + else if (NULL != (alerting = find_call_with_status(CSD_CALL_STATUS_COMING))) + err = reject_call(alerting); + else if (call->conference) + err = release_conference(); + else + err = release_call(call); + + if (err < 0) + telephony_terminate_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_terminate_call_rsp(telephony_device, CME_ERROR_NONE); + DBG("-\n"); + +} + +void telephony_answer_call_req(void *telephony_device) +{ + struct csd_call *call; + + call = find_call_with_status(CSD_CALL_STATUS_COMING); + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_PROCEEDING); + + if (!call) + call = find_call_with_status(CSD_CALL_STATUS_WAITING); + + if (!call) { + telephony_answer_call_rsp(telephony_device, + CME_ERROR_NOT_ALLOWED); + return; + } + + if (answer_call(call) < 0) + telephony_answer_call_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_answer_call_rsp(telephony_device, CME_ERROR_NONE); + +} + +void telephony_key_press_req(void *telephony_device, const char *keys) +{ + struct csd_call *active, *waiting; + int err; + + DBG("telephony-tizen: got key press request for %s", keys); + + waiting = find_call_with_status(CSD_CALL_STATUS_COMING); + if (!waiting) + waiting = find_call_with_status(CSD_CALL_STATUS_MT_ALERTING); + if (!waiting) + waiting = find_call_with_status(CSD_CALL_STATUS_PROCEEDING); + + active = find_call_with_status(CSD_CALL_STATUS_ACTIVE); + + if (waiting) + err = answer_call(waiting); + else if (active) + err = release_call(active); + else { + /*As per the HSP1.2 specification, for user action - Cellular phone can perform + predefined user action. In our case we shall support Last no. dial*/ + dbus_method_call_send(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "DialLastNum", + telephony_dial_number_reply, telephony_device, + DBUS_TYPE_INVALID); + return; + } + + if (err < 0) + telephony_key_press_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + else + telephony_key_press_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_last_dialed_number_req(void *telephony_device) +{ + DBG("telephony-tizen: last dialed number request"); + + if (0 != dbus_method_call_send(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "DialLastNum", + telephony_dial_number_reply, telephony_device, + DBUS_TYPE_INVALID)) { + telephony_dial_number_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + } +} + +void telephony_transmit_dtmf_req(void *telephony_device, char tone) +{ + char buf[2] = { tone, '\0' }, *buf_ptr = buf; + struct csd_call *call; + + DBG("telephony-tizen: transmit dtmf: %s", buf); + + /* Find any Ongoing call, in active/held/waiting */ + if (NULL == (call = find_call_with_status(CSD_CALL_STATUS_ACTIVE))) + if (NULL == (call = find_call_with_status( + CSD_CALL_STATUS_HOLD))) + if (NULL == (call = find_call_with_status( + CSD_CALL_STATUS_WAITING))) { + DBG("No Onging Call \n"); + telephony_transmit_dtmf_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + if (0 != dbus_method_call_send(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "SendDtmf", + NULL, NULL, + DBUS_TYPE_STRING, &buf_ptr, + DBUS_TYPE_STRING, &call->path, + DBUS_TYPE_STRING, &call->sender, + DBUS_TYPE_INVALID)) { + telephony_transmit_dtmf_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_transmit_dtmf_rsp(telephony_device, CME_ERROR_NONE); +} + +static int csd_status_to_hfp(struct csd_call *call) +{ + switch (call->status) { + case CSD_CALL_STATUS_IDLE: + case CSD_CALL_STATUS_MO_RELEASE: + case CSD_CALL_STATUS_MT_RELEASE: + case CSD_CALL_STATUS_TERMINATED: + return -1; + case CSD_CALL_STATUS_CREATE: + return CALL_STATUS_DIALING; + case CSD_CALL_STATUS_WAITING: + return CALL_STATUS_WAITING; + case CSD_CALL_STATUS_PROCEEDING: + /* PROCEEDING can happen in outgoing/incoming */ + if (call->originating) + return CALL_STATUS_DIALING; + /* + * PROCEEDING is followed by WAITING CSD status, therefore + * second incoming call status indication is set immediately + * to waiting. + */ + if (g_slist_length(active_calls) > 0) + return CALL_STATUS_WAITING; + + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_COMING: + if (g_slist_length(active_calls) > 0) + return CALL_STATUS_WAITING; + + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_MO_ALERTING: + return CALL_STATUS_ALERTING; + case CSD_CALL_STATUS_MT_ALERTING: + return CALL_STATUS_INCOMING; + case CSD_CALL_STATUS_ANSWERED: + case CSD_CALL_STATUS_ACTIVE: + case CSD_CALL_STATUS_RECONNECT_PENDING: + case CSD_CALL_STATUS_SWAP_INITIATED: + case CSD_CALL_STATUS_HOLD_INITIATED: + return CALL_STATUS_ACTIVE; + case CSD_CALL_STATUS_RETRIEVE_INITIATED: + case CSD_CALL_STATUS_HOLD: + return CALL_STATUS_HELD; + default: + return -1; + } +} + +void telephony_list_current_calls_req(void *telephony_device) +{ + GSList *l; + int i; + + DBG("telephony-tizen: list current calls request"); + + for (l = calls, i = 1; l != NULL; l = l->next, i++) { + struct csd_call *call = l->data; + int status, direction, multiparty; + + status = csd_status_to_hfp(call); + if (status < 0) + continue; + + direction = call->originating ? + CALL_DIR_OUTGOING : CALL_DIR_INCOMING; + + multiparty = call->conference ? + CALL_MULTIPARTY_YES : CALL_MULTIPARTY_NO; + + telephony_list_current_call_ind(i, direction, status, + CALL_MODE_VOICE, multiparty, + call->number, + number_type(call->number)); + } + + telephony_list_current_calls_rsp(telephony_device, CME_ERROR_NONE); + +} + +static void telephony_operator_reply(DBusPendingCall *call, void *telephony_device) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + DBusError derr; + const gchar *operator_name; + + DBG("telephony_operator_reply\n"); + + dbus_error_init(&derr); + + if (dbus_set_error_from_message(&derr, reply)) { + DBG("telephony_operator_reply error:%s", derr.message); + goto failed; + } + + if (!dbus_message_get_args(reply, NULL, + DBUS_TYPE_STRING, &operator_name, + DBUS_TYPE_INVALID)) + goto failed; + + DBG("telephony_operator_reply -operator_name:%s", operator_name); + net.operator_name = g_strndup(operator_name, 16); + + telephony_operator_selection_ind(OPERATOR_MODE_AUTO, + operator_name); + telephony_operator_selection_rsp(telephony_device, CME_ERROR_NONE); + return; + +failed: + dbus_error_free(&derr); + telephony_operator_selection_ind(OPERATOR_MODE_AUTO, "UNKOWN"); + telephony_operator_selection_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_operator_selection_req(void *telephony_device) +{ + int err = 0; + + err = dbus_method_call_send(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "GetOperatorName", + telephony_operator_reply, telephony_device, + DBUS_TYPE_INVALID); + if (err) + telephony_operator_selection_rsp(telephony_device, + CME_ERROR_AG_FAILURE); +} + +void telephony_nr_and_ec_req(void *telephony_device, gboolean enable) +{ + DBG("telephony-tizen: got %s NR and EC request", + enable ? "enable" : "disable"); + + if (0 != dbus_method_call_send(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "NrecStatus", + NULL, NULL, DBUS_TYPE_BOOLEAN, &enable, + DBUS_TYPE_INVALID)) { + telephony_nr_and_ec_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_nr_and_ec_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_voice_dial_req(void *telephony_device, gboolean enable) +{ + + DBG("telephony-tizen: got %s voice dial request", + enable ? "enable" : "disable"); + + if (0 != dbus_method_call_send(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "VoiceDial", + NULL, NULL, DBUS_TYPE_BOOLEAN, &enable, + DBUS_TYPE_INVALID)) { + telephony_voice_dial_rsp(telephony_device, + CME_ERROR_AG_FAILURE); + return; + } + + telephony_voice_dial_rsp(telephony_device, CME_ERROR_NONE); +} + +void telephony_subscriber_number_req(void *telephony_device) +{ + DBG("telephony-tizen: subscriber number request"); + if (subscriber_number) + telephony_subscriber_number_ind(subscriber_number, + number_type(subscriber_number), + SUBSCRIBER_SERVICE_VOICE); + telephony_subscriber_number_rsp(telephony_device, CME_ERROR_NONE); +} + +static char *get_supported_list(const char *list[], unsigned int size) +{ + GString *str; + int i = 0; + + if (list == NULL || size == 0) + return NULL; + + str = g_string_new("("); + while (i < size) { + if (i > 0) + g_string_append(str, ","); + + g_string_append(str, list[i]); + i++; + } + + g_string_append(str, ")"); + + return g_string_free(str, FALSE); +} + +static int convert_utf8_gsm(uint8_t ascii, uint8_t utf_8, uint8_t *gsm) +{ + uint32_t i; + + if (ascii == 0xC3) { + for (i = 0; i < GSM_UNI_MAX_C3 ; i++) { + if (gsm_unicode_C3[i].utf_8 == utf_8) { + *gsm = gsm_unicode_C3[i].gsm; + return 0; + } + } + } else if (ascii == 0xCE) { + for (i = 0; i < GSM_UNI_MAX_CE ; i++) { + if (gsm_unicode_CE[i].utf_8 == utf_8) { + *gsm = gsm_unicode_CE[i].gsm; + return 0; + } + } + } + + return 1; +} + +static void get_unicode_string(const char *name, char *unicodename) +{ + if (NULL != name && NULL != unicodename) { + int len = strlen(name); + int x, y; + + if (len == 0) + return; + + if (len > PHONEBOOK_MAX_CHARACTER_LENGTH) + len = PHONEBOOK_MAX_CHARACTER_LENGTH; + + for (x = 0, y = 0 ; x < len ; x++, y++) { + if (x < (len - 1)) { + if (convert_utf8_gsm(name[x], name[x+1], + (uint8_t *)&unicodename[y])) { + x++; + continue; + } + } + + if (name[x] == '_') { + unicodename[y] = ' '; + continue; + } + + unicodename[y] = name[x]; + } + } + return; +} + +static int get_phonebook_count(const char *path, uint32_t *max_size, + uint32_t *used) +{ + DBusConnection *conn; + DBusMessage *message, *reply; + DBusError error; + + uint32_t max = 0; + uint32_t size = 0; + + conn = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + if (!conn) { + DBG("Can't get on system bus"); + return -1; + } + + message = dbus_message_new_method_call(PHONEBOOK_BUS_NAME, + PHONEBOOK_PATH, + PHONEBOOK_INTERFACE, + "GetPhonebookSizeAt"); + if (!message) { + DBG("Can't allocate new message"); + dbus_connection_unref(conn); + return -1; + } + + dbus_message_append_args(message, DBUS_TYPE_STRING, &path, + DBUS_TYPE_INVALID); + + dbus_error_init(&error); + + reply = dbus_connection_send_with_reply_and_block(conn, + message, -1, &error); + + if (!reply) { + if (dbus_error_is_set(&error) == TRUE) { + DBG("%s", error.message); + dbus_error_free(&error); + } else { + DBG("Failed to get contacts"); + } + dbus_message_unref(message); + dbus_connection_unref(conn); + return -1; + } + + if (!dbus_message_get_args(reply, &error, + DBUS_TYPE_UINT32, &size, + DBUS_TYPE_INVALID)) { + DBG("Can't get reply arguments\n"); + if (dbus_error_is_set(&error)) { + DBG("%s\n", error.message); + dbus_error_free(&error); + } + dbus_message_unref(reply); + dbus_message_unref(message); + dbus_connection_unref(conn); + return -1; + } + + if ((g_strcmp0(path, "\"SM\"") == 0) || + (g_strcmp0(path, "\"ME\"") == 0)) { + max = PHONEBOOK_COUNT_MAX; + } + if ((g_strcmp0(path, "\"DC\"") == 0) || + (g_strcmp0(path, "\"MC\"") == 0) || + (g_strcmp0(path, "\"RC\"") == 0)) { + max = CALL_LOG_COUNT_MAX; + } + + if (max_size) + *max_size = max; + + if (used) { + if (size > max) + *used = max; + else + *used = size; + } + + dbus_message_unref(reply); + dbus_message_unref(message); + dbus_connection_unref(conn); + + return 0; +} + +static int read_phonebook_entries(int start_index, int end_index) +{ + DBusConnection *conn; + DBusMessage *message, *reply; + DBusMessageIter iter, iter_struct; + DBusError error; + + int count = 0; + + conn = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + if (!conn) { + DBG("Can't get on system bus"); + return -1; + } + + message = dbus_message_new_method_call(PHONEBOOK_BUS_NAME, + PHONEBOOK_PATH, + PHONEBOOK_INTERFACE, + "GetPhonebookEntriesAt"); + if (!message) { + DBG("Can't allocate new message"); + dbus_connection_unref(conn); + return -1; + } + + dbus_message_append_args(message, + DBUS_TYPE_STRING, + &phonebook_store_list[ag_pb_info.path_id], + DBUS_TYPE_INT32, &start_index, + DBUS_TYPE_INT32, &end_index, + DBUS_TYPE_INVALID); + + dbus_error_init(&error); + + reply = dbus_connection_send_with_reply_and_block(conn, + message, -1, &error); + + if (!reply) { + if (dbus_error_is_set(&error) == TRUE) { + DBG("%s", error.message); + dbus_error_free(&error); + } else { + DBG("Failed to get contacts"); + } + + dbus_message_unref(message); + dbus_connection_unref(conn); + + return -1; + } + + dbus_message_iter_init(reply, &iter); + dbus_message_iter_recurse(&iter, &iter_struct); + + while(dbus_message_iter_get_arg_type(&iter_struct) == + DBUS_TYPE_STRUCT) { + const char *name = NULL; + const char *number = NULL; + + char *uni_name; + char *uni_number; + + uint32_t handle = 0; + + DBusMessageIter entry_iter; + + dbus_message_iter_recurse(&iter_struct,&entry_iter); + + dbus_message_iter_get_basic(&entry_iter, &name); + dbus_message_iter_next(&entry_iter); + dbus_message_iter_get_basic(&entry_iter, &number); + dbus_message_iter_next(&entry_iter); + dbus_message_iter_get_basic(&entry_iter, &handle); + dbus_message_iter_next(&entry_iter); + + dbus_message_iter_next(&iter_struct); + + uni_name = g_strndup(name, PHONEBOOK_NAME_MAX_LENGTH); + uni_number = g_strndup(number, PHONEBOOK_NAME_MAX_LENGTH); + + telephony_read_phonebook_entries_ind(uni_name, + uni_number, handle); + + count++; + + g_free(uni_name); + g_free(uni_number); + } + + dbus_message_unref(message); + dbus_message_unref(reply); + dbus_connection_unref(conn); + + return count; +} + +static int find_phonebook_entries(const char *str) +{ + DBusConnection *conn; + DBusMessage *message, *reply; + DBusMessageIter iter, iter_struct; + DBusError error; + + conn = dbus_bus_get(DBUS_BUS_SYSTEM, NULL); + if (!conn) { + DBG("Can't get on system bus"); + return -1; + } + + message = dbus_message_new_method_call(PHONEBOOK_BUS_NAME, + PHONEBOOK_PATH, + PHONEBOOK_INTERFACE, + "GetPhonebookEntriesFindAt"); + if (!message) { + DBG("Can't allocate new message"); + dbus_connection_unref(conn); + return -1; + } + + dbus_message_append_args(message, + DBUS_TYPE_STRING, + &phonebook_store_list[ag_pb_info.path_id], + DBUS_TYPE_STRING, &str, + DBUS_TYPE_INVALID); + + dbus_error_init(&error); + + reply = dbus_connection_send_with_reply_and_block(conn, + message, -1, &error); + + if (!reply) { + if (dbus_error_is_set(&error) == TRUE) { + DBG("%s", error.message); + dbus_error_free(&error); + } else { + DBG("Failed to get contacts"); + } + + dbus_message_unref(message); + dbus_connection_unref(conn); + + return -1; + } + + dbus_message_iter_init(reply, &iter); + dbus_message_iter_recurse(&iter, &iter_struct); + + while(dbus_message_iter_get_arg_type(&iter_struct) == + DBUS_TYPE_STRUCT) { + const char *name = NULL; + const char *number = NULL; + + char *uni_name; + char *uni_number; + + uint32_t handle = 0; + + DBusMessageIter entry_iter; + + dbus_message_iter_recurse(&iter_struct,&entry_iter); + + dbus_message_iter_get_basic(&entry_iter, &name); + dbus_message_iter_next(&entry_iter); + dbus_message_iter_get_basic(&entry_iter, &number); + dbus_message_iter_next(&entry_iter); + dbus_message_iter_get_basic(&entry_iter, &handle); + dbus_message_iter_next(&entry_iter); + + dbus_message_iter_next(&iter_struct); + + uni_name = g_strndup(name, PHONEBOOK_NAME_MAX_LENGTH); + uni_number = g_strndup(number, PHONEBOOK_NAME_MAX_LENGTH); + + telephony_find_phonebook_entries_ind(uni_name, uni_number, handle); + + g_free(uni_name); + g_free(uni_number); + } + + dbus_message_unref(message); + dbus_message_unref(reply); + dbus_connection_unref(conn); + + return 0; +} + +void telephony_select_phonebook_memory_status(void *telephony_device) +{ + int32_t path_id = ag_pb_info.path_id; + uint32_t used; + uint32_t max_size; + + cme_error_t err = CME_ERROR_NONE; + + + DBG("telephony-tizen: telephony_read_phonebook_store\n"); + + if (path_id < 0 || path_id >= PHONEBOOK_STORE_LIST_SIZE) + path_id = 0; + + if (get_phonebook_count(phonebook_store_list[path_id], + &max_size, &used)) + err = CME_ERROR_AG_FAILURE; + + telephony_select_phonebook_memory_status_rsp(telephony_device, + phonebook_store_list[path_id], + max_size, used, + err); +} + +void telephony_select_phonebook_memory_list(void *telephony_device) +{ +/* + For Blue & Me car kit we may have to add the + patch here(similar to the patch done in H1 and H2 ) +*/ + char *str; + + str = get_supported_list(phonebook_store_list, + PHONEBOOK_STORE_LIST_SIZE); + + DBG("telephony-tizen: telephony_select_phonebook_memory_list %d :%s\n", + PHONEBOOK_STORE_LIST_SIZE, str); + + telephony_select_phonebook_memory_list_rsp(telephony_device, + str, CME_ERROR_NONE); + + g_free(str); +} + +void telephony_select_phonebook_memory(void *telephony_device, const gchar *path) +{ + + int i = 0; + cme_error_t err; + + DBG("telephony-tizen: telephony_select_phonebook_memory\n"); + DBG("set phonebook type to [%s]\n", path); + + while (i < PHONEBOOK_STORE_LIST_SIZE) { + if (strcmp(phonebook_store_list[i], path) == 0) + break; + + i++; + } + + if (i >= 0 && i < PHONEBOOK_STORE_LIST_SIZE) { + err = CME_ERROR_NONE; + ag_pb_info.path_id = i; + } else { + err = CME_ERROR_INVALID_TEXT_STRING; + } + telephony_select_phonebook_memory_rsp(telephony_device, err); +} + +void telephony_read_phonebook_entries_list(void *telephony_device) +{ + cme_error_t err = CME_ERROR_NONE; + + int32_t path_id = ag_pb_info.path_id; + uint32_t used; + + DBG("telephony-tizen: telephony_read_phonebook_entries_list\n"); + + if (path_id < 0 || path_id >= PHONEBOOK_STORE_LIST_SIZE) + err = CME_ERROR_INVALID_INDEX; + else { + if (get_phonebook_count(phonebook_store_list[path_id], + NULL, &used) != 0) { + err = CME_ERROR_NOT_ALLOWED; + } + } + + telephony_read_phonebook_entries_list_rsp(telephony_device, used, + PHONEBOOK_NUMBER_MAX_LENGTH, PHONEBOOK_NAME_MAX_LENGTH, + err); +} + +void telephony_read_phonebook_entries(void *telephony_device, const char *cmd) +{ + int start_index = 0; + int end_index = 0; + + int count; + + char *str = NULL; + char *next = NULL; + + cme_error_t err; + + DBG("telephony-tizen: telephony_read_phonebook_entries\n"); + + if (cmd == NULL) + return; + + str = g_strdup(cmd); + next = strchr(str, ','); + + if (next) { + *next = '\0'; + next++; + + end_index = strtol(next, NULL, 10); + } + + start_index = strtol(str, NULL, 10); + + g_free(str); + + count = read_phonebook_entries(start_index, end_index); + + if (count < 0) + err = CME_ERROR_AG_FAILURE; + else if (count == 0) + err = CME_ERROR_INVALID_INDEX; + else + err = CME_ERROR_NONE; + + telephony_read_phonebook_entries_rsp(telephony_device, err); +} + +void telephony_find_phonebook_entries_status(void *telephony_device) +{ + telephony_find_phonebook_entries_status_ind( + PHONEBOOK_NUMBER_MAX_LENGTH, + PHONEBOOK_NAME_MAX_LENGTH); + + telephony_find_phonebook_entries_status_rsp(telephony_device, + CME_ERROR_NONE); +} + +void telephony_find_phonebook_entries(void *telephony_device, const char *cmd) +{ + gchar *st = NULL; + gchar *unquoted = NULL; + + cme_error_t err = CME_ERROR_NONE; + + DBG("telephony-tizen: telephony_find_phonebook_entry\n"); + + /* remove quote and compress */ + st = strchr(cmd, '"'); + if (st == NULL) + unquoted = g_strdup(cmd); + else { + gchar *end = NULL; + + end = strrchr(cmd, '"'); + if(end == NULL) + unquoted = g_strdup(cmd); + else + unquoted = g_strndup(st + 1, end - st - 1); + } + + if (find_phonebook_entries(unquoted)) + err = CME_ERROR_AG_FAILURE; + + telephony_find_phonebook_entries_rsp(telephony_device, err); + + g_free(unquoted); +} + +void telephony_get_preffered_store_capacity(void *telephony_device) +{ + DBG("telephony-tizen: telephony_list_preffered_store_capcity\n"); + + telephony_get_preffered_store_capacity_rsp(telephony_device, + PREFFERED_MESSAGE_STORAGE_MAX, + CME_ERROR_NONE); +} + +void telephony_list_preffered_store(void *telephony_device) +{ + DBG("telephony-tizen: telephony_list_preffered_store_capcity\n"); + + telephony_list_preffered_store_rsp(telephony_device, + PREFFERED_MESSAGE_STORAGE_LIST, + CME_ERROR_NONE); +} + +/* +void telephony_set_preffered_store_capcity(void *telephony_device, const char *cmd) +{ +} +*/ + +void telephony_get_character_set(void *telephony_device) +{ + DBG("telephony-tizen: telephony_get_character_set\n"); + + telephony_supported_character_generic_rsp(telephony_device, + (char *)character_set_list[ag_pb_info.charset_id], + CME_ERROR_NONE); + +} + +void telephony_list_supported_character(void *telephony_device) +{ + char *str; + + str = get_supported_list(character_set_list, + CHARACTER_SET_LIST_SIZE); + + DBG("telephony-tizen: telephony_list_supported_character_set %d :%s\n", + CHARACTER_SET_LIST_SIZE, str); + + telephony_supported_character_generic_rsp(telephony_device, + str, CME_ERROR_NONE); + + g_free(str); +} + +void telephony_set_characterset(void *telephony_device, const char *cmd) +{ + DBG("telephony-tizen: telephony_set_characterset [%s]\n", cmd); + + int i = 0; + + while (i < CHARACTER_SET_LIST_SIZE) { + if (strcmp(character_set_list[i], cmd) == 0) { + telephony_set_characterset_generic_rsp(telephony_device, + CME_ERROR_NONE); + ag_pb_info.charset_id = i; + return; + } + + i++; + } + + telephony_set_characterset_generic_rsp(telephony_device, + CME_ERROR_NOT_SUPPORTED); + return; + +} + +static void telephony_get_battery_property_reply( + DBusPendingCall *call, void *data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + DBusError derr; + int32_t bcs = 0; + int32_t bcl = 0; + + DBG("battery_property_reply"); + + dbus_error_init(&derr); + if (dbus_set_error_from_message(&derr, reply)) { + DBG("battery_property_reply: %s", derr.message); + dbus_error_free(&derr); + telephony_battery_charge_status_rsp(data, bcs, + bcl, CME_ERROR_AG_FAILURE); + goto done; + } + + if (dbus_message_get_args(reply, NULL, + DBUS_TYPE_INT32, &bcs, + DBUS_TYPE_INT32, &bcl, + DBUS_TYPE_INVALID) == FALSE) { + DBG("get_signal_quality_reply: Invalid arguments"); + telephony_battery_charge_status_rsp(data, bcs, + bcl, CME_ERROR_AG_FAILURE); + goto done; + + } + + telephony_battery_charge_status_rsp(data, bcs, + bcl, CME_ERROR_NONE); + +done: + dbus_message_unref(reply); +} + +void telephony_get_battery_property(void *telephony_device) +{ + DBG("telephony-tizen: telephony_get_battery_property\n"); + + if (0 != dbus_method_call_send(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "GetBatteryStatus", + telephony_get_battery_property_reply, + telephony_device, DBUS_TYPE_INVALID)) { + telephony_battery_charge_status_rsp(telephony_device, 0, 0, + CME_ERROR_AG_FAILURE); + } +} + +static void telephony_get_signal_quality_reply(DBusPendingCall *call, + void *data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + DBusError derr; + int32_t rssi = 0; + int32_t ber = 0; + + DBG("get_signal_quality_reply"); + + dbus_error_init(&derr); + if (dbus_set_error_from_message(&derr, reply)) { + DBG("get_signal_quality_reply: %s", derr.message); + dbus_error_free(&derr); + telephony_signal_quality_rsp(data, rssi, + ber, CME_ERROR_AG_FAILURE); + goto done; + } + + if (dbus_message_get_args(reply, NULL, + DBUS_TYPE_INT32, &rssi, + DBUS_TYPE_INT32, &ber, + DBUS_TYPE_INVALID) == FALSE) { + DBG("get_signal_quality_reply: Invalid arguments"); + telephony_signal_quality_rsp(data, rssi, + ber, CME_ERROR_AG_FAILURE); + goto done; + + } + + telephony_signal_quality_rsp(data, rssi, + ber, CME_ERROR_NONE); + +done: + dbus_message_unref(reply); +} + +void telephony_get_signal_quality(void *telephony_device) +{ + DBG("telephony-tizen: telephony_get_signal_quality\n"); + + if (0 != dbus_method_call_send(HFP_AGENT_SERVICE, HFP_AGENT_PATH, + HFP_AGENT_INTERFACE, "GetSignalQuality", + telephony_get_signal_quality_reply, + telephony_device, DBUS_TYPE_INVALID)) { + telephony_signal_quality_rsp(telephony_device, 0, 0, + CME_ERROR_AG_FAILURE); + } + +} diff --git a/audio/telephony.h b/audio/telephony.h new file mode 100644 index 0000000..6b00033 --- /dev/null +++ b/audio/telephony.h @@ -0,0 +1,304 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include +#include + +/* Response and hold values */ +#define BTRH_NOT_SUPPORTED -2 +#define BTRH_NONE -1 +#define BTRH_HOLD 0 +#define BTRH_ACCEPT 1 +#define BTRH_REJECT 2 + +/* HFP feature bits */ +#define AG_FEATURE_THREE_WAY_CALLING 0x0001 +#define AG_FEATURE_EC_ANDOR_NR 0x0002 +#define AG_FEATURE_VOICE_RECOGNITION 0x0004 +#define AG_FEATURE_INBAND_RINGTONE 0x0008 +#define AG_FEATURE_ATTACH_NUMBER_TO_VOICETAG 0x0010 +#define AG_FEATURE_REJECT_A_CALL 0x0020 +#define AG_FEATURE_ENHANCED_CALL_STATUS 0x0040 +#define AG_FEATURE_ENHANCED_CALL_CONTROL 0x0080 +#define AG_FEATURE_EXTENDED_ERROR_RESULT_CODES 0x0100 + +#define HF_FEATURE_EC_ANDOR_NR 0x0001 +#define HF_FEATURE_CALL_WAITING_AND_3WAY 0x0002 +#define HF_FEATURE_CLI_PRESENTATION 0x0004 +#define HF_FEATURE_VOICE_RECOGNITION 0x0008 +#define HF_FEATURE_REMOTE_VOLUME_CONTROL 0x0010 +#define HF_FEATURE_ENHANCED_CALL_STATUS 0x0020 +#define HF_FEATURE_ENHANCED_CALL_CONTROL 0x0040 + +/* Indicator event values */ +#define EV_SERVICE_NONE 0 +#define EV_SERVICE_PRESENT 1 + +#define EV_CALL_INACTIVE 0 +#define EV_CALL_ACTIVE 1 + +#define EV_CALLSETUP_INACTIVE 0 +#define EV_CALLSETUP_INCOMING 1 +#define EV_CALLSETUP_OUTGOING 2 +#define EV_CALLSETUP_ALERTING 3 + +#define EV_CALLHELD_NONE 0 +#define EV_CALLHELD_MULTIPLE 1 +#define EV_CALLHELD_ON_HOLD 2 + +#define EV_ROAM_INACTIVE 0 +#define EV_ROAM_ACTIVE 1 + +/* Call parameters */ +#define CALL_DIR_OUTGOING 0 +#define CALL_DIR_INCOMING 1 + +#define CALL_STATUS_ACTIVE 0 +#define CALL_STATUS_HELD 1 +#define CALL_STATUS_DIALING 2 +#define CALL_STATUS_ALERTING 3 +#define CALL_STATUS_INCOMING 4 +#define CALL_STATUS_WAITING 5 + +#define CALL_MODE_VOICE 0 +#define CALL_MODE_DATA 1 +#define CALL_MODE_FAX 2 + +#define CALL_MULTIPARTY_NO 0 +#define CALL_MULTIPARTY_YES 1 + +/* Subscriber number parameters */ +#define SUBSCRIBER_SERVICE_VOICE 4 +#define SUBSCRIBER_SERVICE_FAX 5 + +/* Operator selection mode values */ +#define OPERATOR_MODE_AUTO 0 +#define OPERATOR_MODE_MANUAL 1 +#define OPERATOR_MODE_DEREGISTER 2 +#define OPERATOR_MODE_MANUAL_AUTO 4 + +/* Some common number types */ +#define NUMBER_TYPE_UNKNOWN 128 +#define NUMBER_TYPE_TELEPHONY 129 +#define NUMBER_TYPE_INTERNATIONAL 145 +#define NUMBER_TYPE_NATIONAL 161 +#define NUMBER_TYPE_VOIP 255 + +/* Extended Audio Gateway Error Result Codes */ +typedef enum { + CME_ERROR_NONE = -1, + CME_ERROR_AG_FAILURE = 0, + CME_ERROR_NO_PHONE_CONNECTION = 1, + CME_ERROR_NOT_ALLOWED = 3, + CME_ERROR_NOT_SUPPORTED = 4, + CME_ERROR_PH_SIM_PIN_REQUIRED = 5, + CME_ERROR_SIM_NOT_INSERTED = 10, + CME_ERROR_SIM_PIN_REQUIRED = 11, + CME_ERROR_SIM_PUK_REQUIRED = 12, + CME_ERROR_SIM_FAILURE = 13, + CME_ERROR_SIM_BUSY = 14, + CME_ERROR_INCORRECT_PASSWORD = 16, + CME_ERROR_SIM_PIN2_REQUIRED = 17, + CME_ERROR_SIM_PUK2_REQUIRED = 18, + CME_ERROR_MEMORY_FULL = 20, + CME_ERROR_INVALID_INDEX = 21, + CME_ERROR_MEMORY_FAILURE = 23, + CME_ERROR_TEXT_STRING_TOO_LONG = 24, + CME_ERROR_INVALID_TEXT_STRING = 25, + CME_ERROR_DIAL_STRING_TOO_LONG = 26, + CME_ERROR_INVALID_DIAL_STRING = 27, + CME_ERROR_NO_NETWORK_SERVICE = 30, + CME_ERROR_NETWORK_TIMEOUT = 31, + CME_ERROR_NETWORK_NOT_ALLOWED = 32, +} cme_error_t; + +struct indicator { + const char *desc; + const char *range; + int val; + gboolean ignore_redundant; +}; + +/* Notify telephony-*.c of connected/disconnected devices. Implemented by + * telephony-*.c + */ +void telephony_device_connected(void *telephony_device); +void telephony_device_disconnected(void *telephony_device); + +/* HF requests (sent by the handsfree device). These are implemented by + * telephony-*.c + */ +void telephony_event_reporting_req(void *telephony_device, int ind); +void telephony_response_and_hold_req(void *telephony_device, int rh); +void telephony_last_dialed_number_req(void *telephony_device); +void telephony_terminate_call_req(void *telephony_device); +void telephony_answer_call_req(void *telephony_device); +void telephony_dial_number_req(void *telephony_device, const char *number); +void telephony_transmit_dtmf_req(void *telephony_device, char tone); +void telephony_subscriber_number_req(void *telephony_device); +void telephony_list_current_calls_req(void *telephony_device); +void telephony_operator_selection_req(void *telephony_device); +void telephony_call_hold_req(void *telephony_device, const char *cmd); +void telephony_nr_and_ec_req(void *telephony_device, gboolean enable); +void telephony_voice_dial_req(void *telephony_device, gboolean enable); +void telephony_key_press_req(void *telephony_device, const char *keys); + +#ifdef __TIZEN_PATCH__ +void telephony_select_phonebook_memory_status(void *telephony_device); +void telephony_select_phonebook_memory_list(void *telephony_device); +void telephony_select_phonebook_memory(void *telephony_device, const gchar *path); +void telephony_read_phonebook_entries_list(void *telephony_device); +void telephony_read_phonebook_entries(void *telephony_device, const char *cmd); +void telephony_find_phonebook_entries_status(void *telephony_device); +void telephony_find_phonebook_entries(void *telephony_device, const char *cmd); +void telephony_get_preffered_store_capacity(void *telephony_device); +void telephony_list_preffered_store(void *telephony_device); +void telephony_get_character_set(void *telephony_device); +void telephony_list_supported_character(void *telephony_device); +void telephony_set_characterset(void *telephony_device, const char *cmd); +void telephony_get_battery_property(void *telephony_device); +void telephony_get_signal_quality(void *telephony_device); + +int telephony_select_phonebook_memory_status_rsp(void *telephony_device, const gchar *path, + uint32_t total, uint32_t used, + cme_error_t err); +int telephony_select_phonebook_memory_list_rsp(void *telephony_device, const char *buf, + cme_error_t err); +int telephony_select_phonebook_memory_rsp(void *telephony_device, cme_error_t err); +int telephony_read_phonebook_entries_list_rsp(void *telephony_device, + uint32_t used, + uint32_t number_length, + uint32_t name_length, + cme_error_t err); +int telephony_find_phonebook_entries_status_rsp(void *telephony_device, + cme_error_t err); +int telephony_read_phonebook_entries_rsp(void *telephony_device, + cme_error_t err); +int telephony_find_phonebook_entries_rsp(void *telephony_device, + cme_error_t err); +int telephony_list_preffered_store_rsp(void *telephony_device, + char *prefrd_list, cme_error_t err); +int telephony_get_preffered_store_capacity_rsp(void *telephony_device, + uint32_t store_capacity, + cme_error_t err); +int telephony_supported_character_generic_rsp(void *telephony_device, + char *character_set_list, + cme_error_t err); +int telephony_set_characterset_generic_rsp(void *telephony_device, + cme_error_t err); +int telephony_battery_charge_status_rsp(void *telephony_device, + int32_t bcs, + int32_t bcl, + cme_error_t err); +int telephony_signal_quality_rsp(void *telephony_device, + int32_t rssi, + int32_t ber, + cme_error_t err); + +int telephony_read_phonebook_entries_ind(const char *name, const char *number, + uint32_t handle); +int telephony_find_phonebook_entries_status_ind( uint32_t number_length, + uint32_t name_length); +int telephony_find_phonebook_entries_ind(const char *name, const char *number, + uint32_t handle); +#endif + +/* AG responses to HF requests. These are implemented by headset.c */ +int telephony_event_reporting_rsp(void *telephony_device, cme_error_t err); +int telephony_response_and_hold_rsp(void *telephony_device, cme_error_t err); +int telephony_last_dialed_number_rsp(void *telephony_device, cme_error_t err); +int telephony_terminate_call_rsp(void *telephony_device, cme_error_t err); +int telephony_answer_call_rsp(void *telephony_device, cme_error_t err); +int telephony_dial_number_rsp(void *telephony_device, cme_error_t err); +int telephony_transmit_dtmf_rsp(void *telephony_device, cme_error_t err); +int telephony_subscriber_number_rsp(void *telephony_device, cme_error_t err); +int telephony_list_current_calls_rsp(void *telephony_device, cme_error_t err); +int telephony_operator_selection_rsp(void *telephony_device, cme_error_t err); +int telephony_call_hold_rsp(void *telephony_device, cme_error_t err); +int telephony_nr_and_ec_rsp(void *telephony_device, cme_error_t err); +int telephony_voice_dial_rsp(void *telephony_device, cme_error_t err); +int telephony_key_press_rsp(void *telephony_device, cme_error_t err); + +/* Event indications by AG. These are implemented by headset.c */ +int telephony_event_ind(int index); +int telephony_response_and_hold_ind(int rh); +int telephony_incoming_call_ind(const char *number, int type); +int telephony_calling_stopped_ind(void); +int telephony_ready_ind(uint32_t features, const struct indicator *indicators, + int rh, const char *chld); +int telephony_deinit(void); +int telephony_list_current_call_ind(int idx, int dir, int status, int mode, + int mprty, const char *number, + int type); +int telephony_subscriber_number_ind(const char *number, int type, + int service); +int telephony_call_waiting_ind(const char *number, int type); +int telephony_operator_selection_ind(int mode, const char *oper); + +/* Helper function for quick indicator updates */ +static inline int telephony_update_indicator(struct indicator *indicators, + const char *desc, + int new_val) +{ + int i; + struct indicator *ind = NULL; + + for (i = 0; indicators[i].desc != NULL; i++) { + if (g_str_equal(indicators[i].desc, desc)) { + ind = &indicators[i]; + break; + } + } + + if (!ind) + return -ENOENT; + + DBG("Telephony indicator \"%s\" %d->%d", desc, ind->val, new_val); + + if (ind->ignore_redundant && ind->val == new_val) { + DBG("Ignoring no-change indication"); + return 0; + } + + ind->val = new_val; + + return telephony_event_ind(i); +} + +static inline int telephony_get_indicator(const struct indicator *indicators, + const char *desc) +{ + int i; + + for (i = 0; indicators[i].desc != NULL; i++) { + if (g_str_equal(indicators[i].desc, desc)) + return indicators[i].val; + } + + return -ENOENT; +} + +int telephony_init(void); +void telephony_exit(void); diff --git a/audio/transport.c b/audio/transport.c new file mode 100644 index 0000000..b015625 --- /dev/null +++ b/audio/transport.c @@ -0,0 +1,1147 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2007 Nokia Corporation + * Copyright (C) 2004-2009 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include +#include + +#include "../src/adapter.h" +#include "../src/dbus-common.h" + +#include "log.h" +#include "error.h" +#include "device.h" +#include "avdtp.h" +#include "media.h" +#include "transport.h" +#include "a2dp.h" +#include "headset.h" +#include "gateway.h" +#include "avrcp.h" + +#define MEDIA_TRANSPORT_INTERFACE "org.bluez.MediaTransport" + +struct media_request { + DBusMessage *msg; + guint id; +}; + +struct media_owner { + struct media_transport *transport; + struct media_request *pending; + char *name; + char *accesstype; + guint watch; +}; + +struct a2dp_transport { + struct avdtp *session; + uint16_t delay; + uint16_t volume; +}; + +struct headset_transport { + struct audio_device *device; + unsigned int nrec_id; +}; + +struct media_transport { + DBusConnection *conn; + char *path; /* Transport object path */ + struct audio_device *device; /* Transport device */ + struct media_endpoint *endpoint; /* Transport endpoint */ + GSList *owners; /* Transport owners */ + uint8_t *configuration; /* Transport configuration */ + int size; /* Transport configuration size */ + int fd; /* Transport file descriptor */ + uint16_t imtu; /* Transport input mtu */ + uint16_t omtu; /* Transport output mtu */ + gboolean read_lock; + gboolean write_lock; + gboolean in_use; + guint (*resume) (struct media_transport *transport, + struct media_owner *owner); + guint (*suspend) (struct media_transport *transport, + struct media_owner *owner); + void (*cancel) (struct media_transport *transport, + guint id); + void (*get_properties) ( + struct media_transport *transport, + DBusMessageIter *dict); + int (*set_property) ( + struct media_transport *transport, + const char *property, + DBusMessageIter *value); + GDestroyNotify destroy; + void *data; +}; + +void media_transport_destroy(struct media_transport *transport) +{ + char *path; + + path = g_strdup(transport->path); + g_dbus_unregister_interface(transport->conn, path, + MEDIA_TRANSPORT_INTERFACE); + + g_free(path); +} + +static struct media_request *media_request_create(DBusMessage *msg, guint id) +{ + struct media_request *req; + + req = g_new0(struct media_request, 1); + req->msg = dbus_message_ref(msg); + req->id = id; + + DBG("Request created: method=%s id=%u", dbus_message_get_member(msg), + id); + + return req; +} + +static void media_request_reply(struct media_request *req, + DBusConnection *conn, int err) +{ + DBusMessage *reply; + + DBG("Request %s Reply %s", dbus_message_get_member(req->msg), + strerror(err)); + + if (!err) + reply = g_dbus_create_reply(req->msg, DBUS_TYPE_INVALID); + else + reply = g_dbus_create_error(req->msg, + ERROR_INTERFACE ".Failed", + "%s", strerror(err)); + + g_dbus_send_message(conn, reply); +} + +static gboolean media_transport_release(struct media_transport *transport, + const char *accesstype) +{ + if (g_strstr_len(accesstype, -1, "r") != NULL) { + transport->read_lock = FALSE; + DBG("Transport %s: read lock released", transport->path); + } + + if (g_strstr_len(accesstype, -1, "w") != NULL) { + transport->write_lock = FALSE; + DBG("Transport %s: write lock released", transport->path); + } + + return TRUE; +} + +static void media_owner_remove(struct media_owner *owner) +{ + struct media_transport *transport = owner->transport; + struct media_request *req = owner->pending; + + if (!req) + return; + + DBG("Owner %s Request %s", owner->name, + dbus_message_get_member(req->msg)); + + if (req->id) + transport->cancel(transport, req->id); + + owner->pending = NULL; + if (req->msg) + dbus_message_unref(req->msg); + + g_free(req); +} + +static void media_owner_free(struct media_owner *owner) +{ + DBG("Owner %s", owner->name); + + media_owner_remove(owner); + + g_free(owner->name); + g_free(owner->accesstype); + g_free(owner); +} + +static void media_transport_remove(struct media_transport *transport, + struct media_owner *owner) +{ + DBG("Transport %s Owner %s", transport->path, owner->name); + + media_transport_release(transport, owner->accesstype); + + /* Reply if owner has a pending request */ + if (owner->pending) + media_request_reply(owner->pending, transport->conn, EIO); + + transport->owners = g_slist_remove(transport->owners, owner); + + if (owner->watch) + g_dbus_remove_watch(transport->conn, owner->watch); + + media_owner_free(owner); + + /* Suspend if there is no longer any owner */ + if (transport->owners == NULL && transport->in_use) + transport->suspend(transport, NULL); +} + +static gboolean media_transport_set_fd(struct media_transport *transport, + int fd, uint16_t imtu, uint16_t omtu) +{ + if (transport->fd == fd) + return TRUE; + + transport->fd = fd; + transport->imtu = imtu; + transport->omtu = omtu; + + info("%s: fd(%d) ready", transport->path, fd); + + return TRUE; +} + +static void a2dp_resume_complete(struct avdtp *session, + struct avdtp_error *err, void *user_data) +{ + struct media_owner *owner = user_data; + struct media_request *req = owner->pending; + struct media_transport *transport = owner->transport; + struct a2dp_sep *sep = media_endpoint_get_sep(transport->endpoint); + struct avdtp_stream *stream; + int fd; + uint16_t imtu, omtu; + gboolean ret; + + req->id = 0; + + if (err) + goto fail; + + stream = a2dp_sep_get_stream(sep); + if (stream == NULL) + goto fail; + + ret = avdtp_stream_get_transport(stream, &fd, &imtu, &omtu, NULL); + if (ret == FALSE) + goto fail; + + media_transport_set_fd(transport, fd, imtu, omtu); + + if (g_strstr_len(owner->accesstype, -1, "r") == NULL) + imtu = 0; + + if (g_strstr_len(owner->accesstype, -1, "w") == NULL) + omtu = 0; + + ret = g_dbus_send_reply(transport->conn, req->msg, + DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_UINT16, &imtu, + DBUS_TYPE_UINT16, &omtu, + DBUS_TYPE_INVALID); + if (ret == FALSE) + goto fail; + + media_owner_remove(owner); + + return; + +fail: + media_transport_remove(transport, owner); +} + +static guint resume_a2dp(struct media_transport *transport, + struct media_owner *owner) +{ + struct a2dp_transport *a2dp = transport->data; + struct media_endpoint *endpoint = transport->endpoint; + struct audio_device *device = transport->device; + struct a2dp_sep *sep = media_endpoint_get_sep(endpoint); + + if (a2dp->session == NULL) { + a2dp->session = avdtp_get(&device->src, &device->dst); + if (a2dp->session == NULL) + return 0; + } + + if (transport->in_use == TRUE) + goto done; + + transport->in_use = a2dp_sep_lock(sep, a2dp->session); + if (transport->in_use == FALSE) + return 0; + +done: + return a2dp_resume(a2dp->session, sep, a2dp_resume_complete, owner); +} + +static void a2dp_suspend_complete(struct avdtp *session, + struct avdtp_error *err, void *user_data) +{ + struct media_owner *owner = user_data; + struct media_transport *transport = owner->transport; + struct a2dp_transport *a2dp = transport->data; + struct a2dp_sep *sep = media_endpoint_get_sep(transport->endpoint); + + /* Release always succeeds */ + if (owner->pending) { + owner->pending->id = 0; + media_request_reply(owner->pending, transport->conn, 0); + media_owner_remove(owner); + } + + a2dp_sep_unlock(sep, a2dp->session); + transport->in_use = FALSE; + media_transport_remove(transport, owner); +} + +static guint suspend_a2dp(struct media_transport *transport, + struct media_owner *owner) +{ + struct a2dp_transport *a2dp = transport->data; + struct media_endpoint *endpoint = transport->endpoint; + struct a2dp_sep *sep = media_endpoint_get_sep(endpoint); + + if (!owner) { + a2dp_sep_unlock(sep, a2dp->session); + transport->in_use = FALSE; + return 0; + } + + return a2dp_suspend(a2dp->session, sep, a2dp_suspend_complete, owner); +} + +static void cancel_a2dp(struct media_transport *transport, guint id) +{ + a2dp_cancel(transport->device, id); +} + +static void headset_resume_complete(struct audio_device *dev, void *user_data) +{ + struct media_owner *owner = user_data; + struct media_request *req = owner->pending; + struct media_transport *transport = owner->transport; + int fd; + uint16_t imtu, omtu; + gboolean ret; + + req->id = 0; + + if (dev == NULL) + goto fail; + + fd = headset_get_sco_fd(dev); + if (fd < 0) + goto fail; + + imtu = 48; + omtu = 48; + + media_transport_set_fd(transport, fd, imtu, omtu); + + if (g_strstr_len(owner->accesstype, -1, "r") == NULL) + imtu = 0; + + if (g_strstr_len(owner->accesstype, -1, "w") == NULL) + omtu = 0; + + ret = g_dbus_send_reply(transport->conn, req->msg, + DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_UINT16, &imtu, + DBUS_TYPE_UINT16, &omtu, + DBUS_TYPE_INVALID); + if (ret == FALSE) + goto fail; + + media_owner_remove(owner); + + return; + +fail: + media_transport_remove(transport, owner); +} + +static guint resume_headset(struct media_transport *transport, + struct media_owner *owner) +{ + struct audio_device *device = transport->device; + + if (transport->in_use == TRUE) + goto done; + + transport->in_use = headset_lock(device, HEADSET_LOCK_READ | + HEADSET_LOCK_WRITE); + if (transport->in_use == FALSE) + return 0; + +done: + return headset_request_stream(device, headset_resume_complete, + owner); +} + +static void headset_suspend_complete(struct audio_device *dev, void *user_data) +{ + struct media_owner *owner = user_data; + struct media_transport *transport = owner->transport; + + /* Release always succeeds */ + if (owner->pending) { + owner->pending->id = 0; + media_request_reply(owner->pending, transport->conn, 0); + media_owner_remove(owner); + } + + headset_unlock(dev, HEADSET_LOCK_READ | HEADSET_LOCK_WRITE); + transport->in_use = FALSE; + media_transport_remove(transport, owner); +} + +static guint suspend_headset(struct media_transport *transport, + struct media_owner *owner) +{ + struct audio_device *device = transport->device; + + if (!owner) { + headset_unlock(device, HEADSET_LOCK_READ | HEADSET_LOCK_WRITE); + transport->in_use = FALSE; + return 0; + } + + return headset_suspend_stream(device, headset_suspend_complete, owner); +} + +static void cancel_headset(struct media_transport *transport, guint id) +{ + headset_cancel_stream(transport->device, id); +} + +static void gateway_resume_complete(struct audio_device *dev, GError *err, + void *user_data) +{ + struct media_owner *owner = user_data; + struct media_request *req = owner->pending; + struct media_transport *transport = owner->transport; + int fd; + uint16_t imtu, omtu; + gboolean ret; + + req->id = 0; + + if (dev == NULL) + goto fail; + + if (err) { + error("Failed to resume gateway: error %s", err->message); + goto fail; + } + + fd = gateway_get_sco_fd(dev); + if (fd < 0) + goto fail; + + imtu = 48; + omtu = 48; + + media_transport_set_fd(transport, fd, imtu, omtu); + + if (g_strstr_len(owner->accesstype, -1, "r") == NULL) + imtu = 0; + + if (g_strstr_len(owner->accesstype, -1, "w") == NULL) + omtu = 0; + + ret = g_dbus_send_reply(transport->conn, req->msg, + DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_UINT16, &imtu, + DBUS_TYPE_UINT16, &omtu, + DBUS_TYPE_INVALID); + if (ret == FALSE) + goto fail; + + media_owner_remove(owner); + + return; + +fail: + media_transport_remove(transport, owner); +} + +static guint resume_gateway(struct media_transport *transport, + struct media_owner *owner) +{ + struct audio_device *device = transport->device; + + if (transport->in_use == TRUE) + goto done; + + transport->in_use = gateway_lock(device, GATEWAY_LOCK_READ | + GATEWAY_LOCK_WRITE); + if (transport->in_use == FALSE) + return 0; + +done: + return gateway_request_stream(device, gateway_resume_complete, + owner); +} + +static gboolean gateway_suspend_complete(gpointer user_data) +{ + struct media_owner *owner = user_data; + struct media_transport *transport = owner->transport; + struct audio_device *device = transport->device; + + /* Release always succeeds */ + if (owner->pending) { + owner->pending->id = 0; + media_request_reply(owner->pending, transport->conn, 0); + media_owner_remove(owner); + } + + gateway_unlock(device, GATEWAY_LOCK_READ | GATEWAY_LOCK_WRITE); + transport->in_use = FALSE; + media_transport_remove(transport, owner); + return FALSE; +} + +static guint suspend_gateway(struct media_transport *transport, + struct media_owner *owner) +{ + struct audio_device *device = transport->device; + static int id = 1; + + if (!owner) { + gateway_unlock(device, GATEWAY_LOCK_READ | GATEWAY_LOCK_WRITE); + transport->in_use = FALSE; + return 0; + } + + gateway_suspend_stream(device); + g_idle_add(gateway_suspend_complete, owner); + return id++; +} + +static void cancel_gateway(struct media_transport *transport, guint id) +{ + gateway_cancel_stream(transport->device, id); +} + +static void media_owner_exit(DBusConnection *connection, void *user_data) +{ + struct media_owner *owner = user_data; + + owner->watch = 0; + + media_owner_remove(owner); + + media_transport_remove(owner->transport, owner); +} + +static gboolean media_transport_acquire(struct media_transport *transport, + const char *accesstype) +{ + gboolean read_lock = FALSE, write_lock = FALSE; + + if (g_strstr_len(accesstype, -1, "r") != NULL) { + if (transport->read_lock == TRUE) + return FALSE; + read_lock = TRUE; + } + + if (g_strstr_len(accesstype, -1, "w") != NULL) { + if (transport->write_lock == TRUE) + return FALSE; + write_lock = TRUE; + } + + /* Check invalid accesstype */ + if (read_lock == FALSE && write_lock == FALSE) + return FALSE; + + if (read_lock) { + transport->read_lock = read_lock; + DBG("Transport %s: read lock acquired", transport->path); + } + + if (write_lock) { + transport->write_lock = write_lock; + DBG("Transport %s: write lock acquired", transport->path); + } + + + return TRUE; +} + +static void media_transport_add(struct media_transport *transport, + struct media_owner *owner) +{ + DBG("Transport %s Owner %s", transport->path, owner->name); + transport->owners = g_slist_append(transport->owners, owner); + owner->transport = transport; + owner->watch = g_dbus_add_disconnect_watch(transport->conn, owner->name, + media_owner_exit, + owner, NULL); +} + +static struct media_owner *media_owner_create(DBusConnection *conn, + DBusMessage *msg, + const char *accesstype) +{ + struct media_owner *owner; + + owner = g_new0(struct media_owner, 1); + owner->name = g_strdup(dbus_message_get_sender(msg)); + owner->accesstype = g_strdup(accesstype); + + DBG("Owner created: sender=%s accesstype=%s", owner->name, + accesstype); + + return owner; +} + +static void media_owner_add(struct media_owner *owner, + struct media_request *req) +{ + DBG("Owner %s Request %s", owner->name, + dbus_message_get_member(req->msg)); + + owner->pending = req; +} + +static struct media_owner *media_transport_find_owner( + struct media_transport *transport, + const char *name) +{ + GSList *l; + + for (l = transport->owners; l; l = l->next) { + struct media_owner *owner = l->data; + + if (g_strcmp0(owner->name, name) == 0) + return owner; + } + + return NULL; +} + +static DBusMessage *acquire(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_transport *transport = data; + struct media_owner *owner; + struct media_request *req; + const char *accesstype, *sender; + guint id; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_STRING, &accesstype, + DBUS_TYPE_INVALID)) + return NULL; + + sender = dbus_message_get_sender(msg); + + owner = media_transport_find_owner(transport, sender); + if (owner != NULL) + return btd_error_not_authorized(msg); + + if (media_transport_acquire(transport, accesstype) == FALSE) + return btd_error_not_authorized(msg); + + owner = media_owner_create(conn, msg, accesstype); + id = transport->resume(transport, owner); + if (id == 0) { + media_transport_release(transport, accesstype); + media_owner_free(owner); + return btd_error_not_authorized(msg); + } + + req = media_request_create(msg, id); + media_owner_add(owner, req); + media_transport_add(transport, owner); + + return NULL; +} + +static DBusMessage *release(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_transport *transport = data; + struct media_owner *owner; + const char *accesstype, *sender; + struct media_request *req; + + if (!dbus_message_get_args(msg, NULL, + DBUS_TYPE_STRING, &accesstype, + DBUS_TYPE_INVALID)) + return NULL; + + sender = dbus_message_get_sender(msg); + + owner = media_transport_find_owner(transport, sender); + if (owner == NULL) + return btd_error_not_authorized(msg); + + if (g_strcmp0(owner->accesstype, accesstype) == 0) { + guint id; + + /* Not the last owner, no need to suspend */ + if (g_slist_length(transport->owners) != 1) { + media_transport_remove(transport, owner); + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); + } + + if (owner->pending) { + const char *member; + + member = dbus_message_get_member(owner->pending->msg); + /* Cancel Acquire request if that exist */ + if (g_str_equal(member, "Acquire")) + media_owner_remove(owner); + else + return btd_error_in_progress(msg); + } + + id = transport->suspend(transport, owner); + if (id == 0) { + media_transport_remove(transport, owner); + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); + } + + req = media_request_create(msg, id); + media_owner_add(owner, req); + + return NULL; + } else if (g_strstr_len(owner->accesstype, -1, accesstype) != NULL) { + media_transport_release(transport, accesstype); + g_strdelimit(owner->accesstype, accesstype, ' '); + } else + return btd_error_not_authorized(msg); + + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static int set_property_a2dp(struct media_transport *transport, + const char *property, + DBusMessageIter *value) +{ + struct a2dp_transport *a2dp = transport->data; + + if (g_strcmp0(property, "Delay") == 0) { + if (dbus_message_iter_get_arg_type(value) != DBUS_TYPE_UINT16) + return -EINVAL; + dbus_message_iter_get_basic(value, &a2dp->delay); + + /* FIXME: send new delay */ + return 0; + } else if (g_strcmp0(property, "Volume") == 0) { + uint16_t volume; + + if (dbus_message_iter_get_arg_type(value) != DBUS_TYPE_UINT16) + return -EINVAL; + + dbus_message_iter_get_basic(value, &volume); + + if (volume > 127) + return -EINVAL; + + if (a2dp->volume == volume) + return 0; + + return avrcp_set_volume(transport->device, volume); + } + + return -EINVAL; +} + +static int set_property_headset(struct media_transport *transport, + const char *property, + DBusMessageIter *value) +{ + if (g_strcmp0(property, "NREC") == 0) { + gboolean nrec; + + if (dbus_message_iter_get_arg_type(value) != DBUS_TYPE_BOOLEAN) + return -EINVAL; + dbus_message_iter_get_basic(value, &nrec); + + /* FIXME: set new nrec */ + return 0; + } else if (g_strcmp0(property, "InbandRingtone") == 0) { + gboolean inband; + + if (dbus_message_iter_get_arg_type(value) != DBUS_TYPE_BOOLEAN) + return -EINVAL; + dbus_message_iter_get_basic(value, &inband); + + /* FIXME: set new inband */ + return 0; + } + + return -EINVAL; +} + +static int set_property_gateway(struct media_transport *transport, + const char *property, + DBusMessageIter *value) +{ + return -EINVAL; +} + +static DBusMessage *set_property(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_transport *transport = data; + DBusMessageIter iter; + DBusMessageIter value; + const char *property, *sender; + GSList *l; + int err; + + if (!dbus_message_iter_init(msg, &iter)) + return btd_error_invalid_args(msg); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) + return btd_error_invalid_args(msg); + + dbus_message_iter_get_basic(&iter, &property); + dbus_message_iter_next(&iter); + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) + return btd_error_invalid_args(msg); + dbus_message_iter_recurse(&iter, &value); + + sender = dbus_message_get_sender(msg); + err = -EINVAL; + + /* Check if sender has acquired the transport */ + for (l = transport->owners; l; l = l->next) { + struct media_owner *owner = l->data; + + if (g_strcmp0(owner->name, sender) == 0) { + err = transport->set_property(transport, property, + &value); + break; + } + } + + if (err < 0) { + if (err == -EINVAL) + return btd_error_invalid_args(msg); + return btd_error_failed(msg, strerror(-err)); + } + + return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); +} + +static void get_properties_a2dp(struct media_transport *transport, + DBusMessageIter *dict) +{ + struct a2dp_transport *a2dp = transport->data; + + dict_append_entry(dict, "Delay", DBUS_TYPE_UINT16, &a2dp->delay); + + if (a2dp->volume <= 127) + dict_append_entry(dict, "Volume", DBUS_TYPE_UINT16, + &a2dp->volume); +} + +static void get_properties_headset(struct media_transport *transport, + DBusMessageIter *dict) +{ + gboolean nrec, inband; + const char *routing; + + nrec = headset_get_nrec(transport->device); + dict_append_entry(dict, "NREC", DBUS_TYPE_BOOLEAN, &nrec); + + inband = headset_get_inband(transport->device); + dict_append_entry(dict, "InbandRingtone", DBUS_TYPE_BOOLEAN, &inband); + + routing = headset_get_sco_hci(transport->device) ? "HCI" : "PCM"; + dict_append_entry(dict, "Routing", DBUS_TYPE_STRING, &routing); +} + +static void get_properties_gateway(struct media_transport *transport, + DBusMessageIter *dict) +{ + /* None */ +} + +void transport_get_properties(struct media_transport *transport, + DBusMessageIter *iter) +{ + DBusMessageIter dict; + const char *uuid; + uint8_t codec; + + dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, + DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING + DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING + DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); + + /* Device */ + dict_append_entry(&dict, "Device", DBUS_TYPE_OBJECT_PATH, + &transport->device->path); + + uuid = media_endpoint_get_uuid(transport->endpoint); + dict_append_entry(&dict, "UUID", DBUS_TYPE_STRING, &uuid); + + codec = media_endpoint_get_codec(transport->endpoint); + dict_append_entry(&dict, "Codec", DBUS_TYPE_BYTE, &codec); + + dict_append_array(&dict, "Configuration", DBUS_TYPE_BYTE, + &transport->configuration, transport->size); + + if (transport->get_properties) + transport->get_properties(transport, &dict); + + dbus_message_iter_close_container(iter, &dict); +} + +static DBusMessage *get_properties(DBusConnection *conn, DBusMessage *msg, + void *data) +{ + struct media_transport *transport = data; + DBusMessage *reply; + DBusMessageIter iter; + + reply = dbus_message_new_method_return(msg); + if (!reply) + return NULL; + + dbus_message_iter_init_append(reply, &iter); + + transport_get_properties(transport, &iter); + + return reply; +} + +static const GDBusMethodTable transport_methods[] = { + { GDBUS_METHOD("GetProperties", + NULL, GDBUS_ARGS({ "properties", "a{sv}" }), + get_properties) }, + { GDBUS_ASYNC_METHOD("Acquire", + GDBUS_ARGS({ "access_type", "s" }), + GDBUS_ARGS({ "fd", "h" }, { "mtu_r", "q" }, + { "mtu_w", "q" } ), + acquire) }, + { GDBUS_ASYNC_METHOD("Release", + GDBUS_ARGS({ "access_type", "s" }), NULL, + release ) }, + { GDBUS_ASYNC_METHOD("SetProperty", + GDBUS_ARGS({ "name", "s" }, { "value", "v" }), + NULL, set_property) }, + { }, +}; + +static const GDBusSignalTable transport_signals[] = { + { GDBUS_SIGNAL("PropertyChanged", + GDBUS_ARGS({ "name", "s" }, { "value", "v" })) }, + { } +}; + +static void destroy_a2dp(void *data) +{ + struct a2dp_transport *a2dp = data; + + if (a2dp->session) + avdtp_unref(a2dp->session); + + g_free(a2dp); +} + +static void destroy_headset(void *data) +{ + struct headset_transport *headset = data; + + if (headset->nrec_id > 0) + headset_remove_nrec_cb(headset->device, headset->nrec_id); + + g_free(headset); +} + +static void media_transport_free(void *data) +{ + struct media_transport *transport = data; + GSList *l = transport->owners; + + while (l) { + struct media_owner *owner = l->data; + l = l->next; + media_transport_remove(transport, owner); + } + + g_slist_free(transport->owners); + + if (transport->destroy != NULL) + transport->destroy(transport->data); + + if (transport->conn) + dbus_connection_unref(transport->conn); + + g_free(transport->configuration); + g_free(transport->path); + g_free(transport); +} + +static void headset_nrec_changed(struct audio_device *dev, gboolean nrec, + void *user_data) +{ + struct media_transport *transport = user_data; + + DBG(""); + + emit_property_changed(transport->conn, transport->path, + MEDIA_TRANSPORT_INTERFACE, "NREC", + DBUS_TYPE_BOOLEAN, &nrec); +} + +struct media_transport *media_transport_create(DBusConnection *conn, + struct media_endpoint *endpoint, + struct audio_device *device, + uint8_t *configuration, + size_t size) +{ + struct media_transport *transport; + const char *uuid; + static int fd = 0; + + transport = g_new0(struct media_transport, 1); + transport->conn = dbus_connection_ref(conn); + transport->device = device; + transport->endpoint = endpoint; + transport->configuration = g_new(uint8_t, size); + memcpy(transport->configuration, configuration, size); + transport->size = size; + transport->path = g_strdup_printf("%s/fd%d", device->path, fd++); + transport->fd = -1; + + uuid = media_endpoint_get_uuid(endpoint); + if (strcasecmp(uuid, A2DP_SOURCE_UUID) == 0 || + strcasecmp(uuid, A2DP_SINK_UUID) == 0) { + struct a2dp_transport *a2dp; + + a2dp = g_new0(struct a2dp_transport, 1); + a2dp->volume = -1; + + transport->resume = resume_a2dp; + transport->suspend = suspend_a2dp; + transport->cancel = cancel_a2dp; + transport->get_properties = get_properties_a2dp; + transport->set_property = set_property_a2dp; + transport->data = a2dp; + transport->destroy = destroy_a2dp; + } else if (strcasecmp(uuid, HFP_AG_UUID) == 0 || + strcasecmp(uuid, HSP_AG_UUID) == 0) { + struct headset_transport *headset; + + headset = g_new0(struct headset_transport, 1); + headset->device = device; + headset->nrec_id = headset_add_nrec_cb(device, + headset_nrec_changed, + transport); + + transport->resume = resume_headset; + transport->suspend = suspend_headset; + transport->cancel = cancel_headset; + transport->get_properties = get_properties_headset; + transport->set_property = set_property_headset; + transport->data = headset; + transport->destroy = destroy_headset; + } else if (strcasecmp(uuid, HFP_HS_UUID) == 0 || + strcasecmp(uuid, HSP_HS_UUID) == 0) { + transport->resume = resume_gateway; + transport->suspend = suspend_gateway; + transport->cancel = cancel_gateway; + transport->get_properties = get_properties_gateway; + transport->set_property = set_property_gateway; + } else + goto fail; + + if (g_dbus_register_interface(transport->conn, transport->path, + MEDIA_TRANSPORT_INTERFACE, + transport_methods, transport_signals, NULL, + transport, media_transport_free) == FALSE) { + error("Could not register transport %s", transport->path); + goto fail; + } + + return transport; + +fail: + media_transport_free(transport); + return NULL; +} + +const char *media_transport_get_path(struct media_transport *transport) +{ + return transport->path; +} + +void media_transport_update_delay(struct media_transport *transport, + uint16_t delay) +{ + struct a2dp_transport *a2dp = transport->data; + + /* Check if delay really changed */ + if (a2dp->delay == delay) + return; + + a2dp->delay = delay; + + emit_property_changed(transport->conn, transport->path, + MEDIA_TRANSPORT_INTERFACE, "Delay", + DBUS_TYPE_UINT16, &a2dp->delay); +} + +struct audio_device *media_transport_get_dev(struct media_transport *transport) +{ + return transport->device; +} + +void media_transport_update_volume(struct media_transport *transport, + uint8_t volume) +{ + struct a2dp_transport *a2dp = transport->data; + + /* Check if volume really changed */ + if (a2dp->volume == volume) + return; + + a2dp->volume = volume; + + emit_property_changed(transport->conn, transport->path, + MEDIA_TRANSPORT_INTERFACE, "Volume", + DBUS_TYPE_UINT16, &a2dp->volume); +} diff --git a/audio/transport.h b/audio/transport.h new file mode 100644 index 0000000..d20c327 --- /dev/null +++ b/audio/transport.h @@ -0,0 +1,41 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2007 Nokia Corporation + * Copyright (C) 2004-2009 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +struct media_transport; + +struct media_transport *media_transport_create(DBusConnection *conn, + struct media_endpoint *endpoint, + struct audio_device *device, + uint8_t *configuration, + size_t size); + +void media_transport_destroy(struct media_transport *transport); +const char *media_transport_get_path(struct media_transport *transport); +struct audio_device *media_transport_get_dev(struct media_transport *transport); +void media_transport_update_delay(struct media_transport *transport, + uint16_t delay); +void media_transport_update_volume(struct media_transport *transport, + uint8_t volume); +void transport_get_properties(struct media_transport *transport, + DBusMessageIter *iter); diff --git a/audio/unix.c b/audio/unix.c new file mode 100644 index 0000000..9a10764 --- /dev/null +++ b/audio/unix.c @@ -0,0 +1,1909 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "log.h" +#include "ipc.h" +#include "device.h" +#include "manager.h" +#include "avdtp.h" +#include "media.h" +#include "a2dp.h" +#include "headset.h" +#include "sink.h" +#include "source.h" +#include "gateway.h" +#include "unix.h" + +#define check_nul(str) (str[sizeof(str) - 1] == '\0') + +typedef enum { + TYPE_NONE, + TYPE_HEADSET, + TYPE_GATEWAY, + TYPE_SINK, + TYPE_SOURCE +} service_type_t; + +typedef void (*notify_cb_t) (struct audio_device *dev, void *data); + +struct a2dp_data { + struct avdtp *session; + struct avdtp_stream *stream; + struct a2dp_sep *sep; +}; + +struct headset_data { + gboolean locked; +}; + +struct unix_client { + struct audio_device *dev; + GSList *caps; + service_type_t type; + char *interface; + uint8_t seid; + union { + struct a2dp_data a2dp; + struct headset_data hs; + } d; + int sock; + int lock; + int data_fd; /* To be deleted once two phase configuration is fully implemented */ + unsigned int req_id; + unsigned int cb_id; + gboolean (*cancel) (struct audio_device *dev, unsigned int id); +}; + +static GSList *clients = NULL; + +static int unix_sock = -1; + +static void client_free(void *data) +{ + struct unix_client *client = data; + + DBG("client_free(%p)", client); + + if (client->cancel && client->dev && client->req_id > 0) + client->cancel(client->dev, client->req_id); + + if (client->sock >= 0) + close(client->sock); + + g_slist_free_full(client->caps, g_free); + + g_free(client->interface); + g_free(client); +} + +static int set_nonblocking(int fd) +{ + long arg; + + arg = fcntl(fd, F_GETFL); + if (arg < 0) + return -errno; + + /* Return if already nonblocking */ + if (arg & O_NONBLOCK) + return 0; + + arg |= O_NONBLOCK; + if (fcntl(fd, F_SETFL, arg) < 0) + return -errno; + + return 0; +} + +/* Pass file descriptor through local domain sockets (AF_LOCAL, formerly + * AF_UNIX) and the sendmsg() system call with the cmsg_type field of a "struct + * cmsghdr" set to SCM_RIGHTS and the data being an integer value equal to the + * handle of the file descriptor to be passed. */ +static int unix_sendmsg_fd(int sock, int fd) +{ + char cmsg_b[CMSG_SPACE(sizeof(int))], m = 'm'; + struct cmsghdr *cmsg; + struct iovec iov = { &m, sizeof(m) }; + struct msghdr msgh; + + memset(&msgh, 0, sizeof(msgh)); + msgh.msg_iov = &iov; + msgh.msg_iovlen = 1; + msgh.msg_control = &cmsg_b; + msgh.msg_controllen = CMSG_LEN(sizeof(int)); + + cmsg = CMSG_FIRSTHDR(&msgh); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + /* Initialize the payload */ + memcpy(CMSG_DATA(cmsg), &fd, sizeof(int)); + + return sendmsg(sock, &msgh, MSG_NOSIGNAL); +} + +static void unix_ipc_sendmsg(struct unix_client *client, + const bt_audio_msg_header_t *msg) +{ + const char *type = bt_audio_strtype(msg->type); + const char *name = bt_audio_strname(msg->name); + + DBG("Audio API: %s -> %s", type, name); + + if (send(client->sock, msg, msg->length, 0) < 0) + error("Error %s(%d)", strerror(errno), errno); +} + +static void unix_ipc_error(struct unix_client *client, uint8_t name, int err) +{ + char buf[BT_SUGGESTED_BUFFER_SIZE]; + bt_audio_error_t *rsp = (void *) buf; + + if (!g_slist_find(clients, client)) + return; + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_ERROR; + rsp->h.name = name; + rsp->h.length = sizeof(*rsp); + + rsp->posix_errno = err; + + DBG("sending error %s(%d)", strerror(err), err); + unix_ipc_sendmsg(client, &rsp->h); +} + +static service_type_t select_service(struct audio_device *dev, const char *interface) +{ + if (!interface) { + if (dev->sink && avdtp_is_connected(&dev->src, &dev->dst)) + return TYPE_SINK; + else if (dev->source && avdtp_is_connected(&dev->src, + &dev->dst)) + return TYPE_SOURCE; + else if (dev->headset && headset_is_active(dev)) + return TYPE_HEADSET; + else if (dev->sink) + return TYPE_SINK; + else if (dev->source) + return TYPE_SOURCE; + else if (dev->headset) + return TYPE_HEADSET; + } else if (!strcmp(interface, AUDIO_SOURCE_INTERFACE) && dev->source) + return TYPE_SOURCE; + else if (!strcmp(interface, AUDIO_SINK_INTERFACE) && dev->sink) + return TYPE_SINK; + else if (!strcmp(interface, AUDIO_HEADSET_INTERFACE) && dev->headset) + return TYPE_HEADSET; + else if (!strcmp(interface, AUDIO_GATEWAY_INTERFACE) && dev->gateway) + return TYPE_GATEWAY; + + return TYPE_NONE; +} + +static void stream_state_changed(struct avdtp_stream *stream, + avdtp_state_t old_state, + avdtp_state_t new_state, + struct avdtp_error *err, + void *user_data) +{ + struct unix_client *client = user_data; + struct a2dp_data *a2dp = &client->d.a2dp; + + switch (new_state) { + case AVDTP_STATE_IDLE: + if (a2dp->sep) { + a2dp_sep_unlock(a2dp->sep, a2dp->session); + a2dp->sep = NULL; + } + if (a2dp->session) { + avdtp_unref(a2dp->session); + a2dp->session = NULL; + } + a2dp->stream = NULL; + client->cb_id = 0; + break; + default: + break; + } +} + +static uint8_t headset_generate_capability(struct audio_device *dev, + codec_capabilities_t *codec) +{ + pcm_capabilities_t *pcm; + + codec->seid = BT_A2DP_SEID_RANGE + 1; + codec->transport = BT_CAPABILITIES_TRANSPORT_SCO; + codec->type = BT_HFP_CODEC_PCM; + codec->length = sizeof(*pcm); + + pcm = (void *) codec; + pcm->sampling_rate = 8000; + if (dev->headset) { + if (headset_get_nrec(dev)) + pcm->flags |= BT_PCM_FLAG_NREC; + if (!headset_get_sco_hci(dev)) + pcm->flags |= BT_PCM_FLAG_PCM_ROUTING; + codec->configured = headset_is_active(dev); + codec->lock = headset_get_lock(dev); + } else { + pcm->flags |= BT_PCM_FLAG_NREC; + codec->configured = TRUE; + codec->lock = 0; + } + + return codec->length; +} + +static void headset_discovery_complete(struct audio_device *dev, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_get_capabilities_rsp *rsp = (void *) buf; + uint8_t length; + + client->req_id = 0; + + if (!dev) + goto failed; + + memset(buf, 0, sizeof(buf)); + + length = headset_generate_capability(dev, (void *) rsp->data); + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_GET_CAPABILITIES; + rsp->h.length = sizeof(*rsp) + length; + + ba2str(&dev->src, rsp->source); + ba2str(&dev->dst, rsp->destination); + strncpy(rsp->object, dev->path, sizeof(rsp->object)); + + unix_ipc_sendmsg(client, &rsp->h); + + return; + +failed: + error("discovery failed"); + unix_ipc_error(client, BT_SET_CONFIGURATION, EIO); +} + +static void headset_setup_complete(struct audio_device *dev, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_set_configuration_rsp *rsp = (void *) buf; + + client->req_id = 0; + + if (!dev) + goto failed; + + memset(buf, 0, sizeof(buf)); + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_SET_CONFIGURATION; + rsp->h.length = sizeof(*rsp); + + rsp->link_mtu = 48; + + client->data_fd = headset_get_sco_fd(dev); + + unix_ipc_sendmsg(client, &rsp->h); + + return; + +failed: + error("config failed"); + unix_ipc_error(client, BT_SET_CONFIGURATION, EIO); +} + +static void gateway_setup_complete(struct audio_device *dev, GError *err, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_set_configuration_rsp *rsp = (void *) buf; + + if (err) { + unix_ipc_error(client, BT_SET_CONFIGURATION, err->code); + return; + } + + client->req_id = 0; + + memset(buf, 0, sizeof(buf)); + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_SET_CONFIGURATION; + rsp->h.length = sizeof(*rsp); + + rsp->link_mtu = 48; + + client->data_fd = gateway_get_sco_fd(dev); + + unix_ipc_sendmsg(client, &rsp->h); +} + +static void headset_resume_complete(struct audio_device *dev, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_start_stream_rsp *rsp = (void *) buf; + struct bt_new_stream_ind *ind = (void *) buf; + + client->req_id = 0; + + if (!dev) + goto failed; + + client->data_fd = headset_get_sco_fd(dev); + if (client->data_fd < 0) { + error("Unable to get a SCO fd"); + goto failed; + } + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_START_STREAM; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + memset(buf, 0, sizeof(buf)); + ind->h.type = BT_INDICATION; + ind->h.name = BT_NEW_STREAM; + ind->h.length = sizeof(*ind); + + unix_ipc_sendmsg(client, &ind->h); + + if (unix_sendmsg_fd(client->sock, client->data_fd) < 0) { + error("unix_sendmsg_fd: %s(%d)", strerror(errno), errno); + goto failed; + } + + return; + +failed: + error("headset_resume_complete: resume failed"); + unix_ipc_error(client, BT_START_STREAM, EIO); +} + +static void gateway_resume_complete(struct audio_device *dev, GError *err, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_start_stream_rsp *rsp = (void *) buf; + struct bt_new_stream_ind *ind = (void *) buf; + + if (err) { + unix_ipc_error(client, BT_START_STREAM, err->code); + return; + } + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_START_STREAM; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + memset(buf, 0, sizeof(buf)); + ind->h.type = BT_INDICATION; + ind->h.name = BT_NEW_STREAM; + ind->h.length = sizeof(*ind); + + unix_ipc_sendmsg(client, &ind->h); + + client->data_fd = gateway_get_sco_fd(dev); + if (unix_sendmsg_fd(client->sock, client->data_fd) < 0) { + error("unix_sendmsg_fd: %s(%d)", strerror(errno), errno); + unix_ipc_error(client, BT_START_STREAM, EIO); + } + + client->req_id = 0; +} + +static void headset_suspend_complete(struct audio_device *dev, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_stop_stream_rsp *rsp = (void *) buf; + + if (!dev) + goto failed; + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_STOP_STREAM; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + return; + +failed: + error("suspend failed"); + unix_ipc_error(client, BT_STOP_STREAM, EIO); +} + +static void print_mpeg12(struct mpeg_codec_cap *mpeg) +{ + DBG("Media Codec: MPEG12" + " Channel Modes: %s%s%s%s" + " Frequencies: %s%s%s%s%s%s" + " Layers: %s%s%s" + " CRC: %s", + mpeg->channel_mode & MPEG_CHANNEL_MODE_MONO ? "Mono " : "", + mpeg->channel_mode & MPEG_CHANNEL_MODE_DUAL_CHANNEL ? + "DualChannel " : "", + mpeg->channel_mode & MPEG_CHANNEL_MODE_STEREO ? "Stereo " : "", + mpeg->channel_mode & MPEG_CHANNEL_MODE_JOINT_STEREO ? + "JointStereo " : "", + mpeg->frequency & MPEG_SAMPLING_FREQ_16000 ? "16Khz " : "", + mpeg->frequency & MPEG_SAMPLING_FREQ_22050 ? "22.05Khz " : "", + mpeg->frequency & MPEG_SAMPLING_FREQ_24000 ? "24Khz " : "", + mpeg->frequency & MPEG_SAMPLING_FREQ_32000 ? "32Khz " : "", + mpeg->frequency & MPEG_SAMPLING_FREQ_44100 ? "44.1Khz " : "", + mpeg->frequency & MPEG_SAMPLING_FREQ_48000 ? "48Khz " : "", + mpeg->layer & MPEG_LAYER_MP1 ? "1 " : "", + mpeg->layer & MPEG_LAYER_MP2 ? "2 " : "", + mpeg->layer & MPEG_LAYER_MP3 ? "3 " : "", + mpeg->crc ? "Yes" : "No"); +} + +static void print_sbc(struct sbc_codec_cap *sbc) +{ + DBG("Media Codec: SBC" + " Channel Modes: %s%s%s%s" + " Frequencies: %s%s%s%s" + " Subbands: %s%s" + " Blocks: %s%s%s%s" + " Bitpool: %d-%d", + sbc->channel_mode & SBC_CHANNEL_MODE_MONO ? "Mono " : "", + sbc->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL ? + "DualChannel " : "", + sbc->channel_mode & SBC_CHANNEL_MODE_STEREO ? "Stereo " : "", + sbc->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO ? "JointStereo" : "", + sbc->frequency & SBC_SAMPLING_FREQ_16000 ? "16Khz " : "", + sbc->frequency & SBC_SAMPLING_FREQ_32000 ? "32Khz " : "", + sbc->frequency & SBC_SAMPLING_FREQ_44100 ? "44.1Khz " : "", + sbc->frequency & SBC_SAMPLING_FREQ_48000 ? "48Khz " : "", + sbc->subbands & SBC_SUBBANDS_4 ? "4 " : "", + sbc->subbands & SBC_SUBBANDS_8 ? "8 " : "", + sbc->block_length & SBC_BLOCK_LENGTH_4 ? "4 " : "", + sbc->block_length & SBC_BLOCK_LENGTH_8 ? "8 " : "", + sbc->block_length & SBC_BLOCK_LENGTH_12 ? "12 " : "", + sbc->block_length & SBC_BLOCK_LENGTH_16 ? "16 " : "", + sbc->min_bitpool, sbc->max_bitpool); +} + +static int a2dp_append_codec(struct bt_get_capabilities_rsp *rsp, + struct avdtp_service_capability *cap, + uint8_t seid, + uint8_t type, + uint8_t configured, + uint8_t lock) +{ + struct avdtp_media_codec_capability *codec_cap = (void *) cap->data; + codec_capabilities_t *codec = (void *) rsp + rsp->h.length; + size_t space_left; + + if (rsp->h.length > BT_SUGGESTED_BUFFER_SIZE) + return -ENOMEM; + + space_left = BT_SUGGESTED_BUFFER_SIZE - rsp->h.length; + + /* endianness prevents direct cast */ + if (codec_cap->media_codec_type == A2DP_CODEC_SBC) { + struct sbc_codec_cap *sbc_cap = (void *) codec_cap; + sbc_capabilities_t *sbc = (void *) codec; + + if (space_left < sizeof(sbc_capabilities_t)) + return -ENOMEM; + + if (type == AVDTP_SEP_TYPE_SINK) + codec->type = BT_A2DP_SBC_SINK; + else if (type == AVDTP_SEP_TYPE_SOURCE) + codec->type = BT_A2DP_SBC_SOURCE; + else + return -EINVAL; + + codec->length = sizeof(sbc_capabilities_t); + + sbc->channel_mode = sbc_cap->channel_mode; + sbc->frequency = sbc_cap->frequency; + sbc->allocation_method = sbc_cap->allocation_method; + sbc->subbands = sbc_cap->subbands; + sbc->block_length = sbc_cap->block_length; + sbc->min_bitpool = sbc_cap->min_bitpool; + sbc->max_bitpool = sbc_cap->max_bitpool; + + print_sbc(sbc_cap); + } else if (codec_cap->media_codec_type == A2DP_CODEC_MPEG12) { + struct mpeg_codec_cap *mpeg_cap = (void *) codec_cap; + mpeg_capabilities_t *mpeg = (void *) codec; + + if (space_left < sizeof(mpeg_capabilities_t)) + return -ENOMEM; + + if (type == AVDTP_SEP_TYPE_SINK) + codec->type = BT_A2DP_MPEG12_SINK; + else if (type == AVDTP_SEP_TYPE_SOURCE) + codec->type = BT_A2DP_MPEG12_SOURCE; + else + return -EINVAL; + + codec->length = sizeof(mpeg_capabilities_t); + + mpeg->channel_mode = mpeg_cap->channel_mode; + mpeg->crc = mpeg_cap->crc; + mpeg->layer = mpeg_cap->layer; + mpeg->frequency = mpeg_cap->frequency; + mpeg->mpf = mpeg_cap->mpf; + mpeg->bitrate = mpeg_cap->bitrate; + + print_mpeg12(mpeg_cap); + } else { + size_t codec_length, type_length, total_length; + + codec_length = cap->length - (sizeof(struct avdtp_service_capability) + + sizeof(struct avdtp_media_codec_capability)); + type_length = sizeof(codec_cap->media_codec_type); + total_length = type_length + codec_length + + sizeof(codec_capabilities_t); + + if (space_left < total_length) + return -ENOMEM; + + if (type == AVDTP_SEP_TYPE_SINK) + codec->type = BT_A2DP_UNKNOWN_SINK; + else if (type == AVDTP_SEP_TYPE_SOURCE) + codec->type = BT_A2DP_UNKNOWN_SOURCE; + else + return -EINVAL; + + codec->length = total_length; + memcpy(codec->data, &codec_cap->media_codec_type, type_length); + memcpy(codec->data + type_length, codec_cap->data, + codec_length); + } + + codec->seid = seid; + codec->configured = configured; + codec->lock = lock; + rsp->h.length += codec->length; + + DBG("Append %s seid %d - length %d - total %d", + configured ? "configured" : "", seid, codec->length, + rsp->h.length); + + return 0; +} + +static void a2dp_discovery_complete(struct avdtp *session, GSList *seps, + struct avdtp_error *err, + void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_get_capabilities_rsp *rsp = (void *) buf; + struct a2dp_data *a2dp = &client->d.a2dp; + + if (!g_slist_find(clients, client)) { + DBG("Client disconnected during discovery"); + return; + } + + if (err) + goto failed; + + memset(buf, 0, sizeof(buf)); + client->req_id = 0; + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_GET_CAPABILITIES; + rsp->h.length = sizeof(*rsp); + ba2str(&client->dev->src, rsp->source); + ba2str(&client->dev->dst, rsp->destination); + strncpy(rsp->object, client->dev->path, sizeof(rsp->object)); + + for (; seps; seps = g_slist_next(seps)) { + struct avdtp_remote_sep *rsep = seps->data; + struct a2dp_sep *sep; + struct avdtp_service_capability *cap; + struct avdtp_stream *stream; + uint8_t type, seid, configured = 0, lock = 0; + GSList *cl; + + type = avdtp_get_type(rsep); + + if (type != AVDTP_SEP_TYPE_SINK && + type != AVDTP_SEP_TYPE_SOURCE) + continue; + + cap = avdtp_get_codec(rsep); + + if (cap->category != AVDTP_MEDIA_CODEC) + continue; + + seid = avdtp_get_seid(rsep); + + if (client->seid != 0 && client->seid != seid) + continue; + + stream = avdtp_get_stream(rsep); + if (stream) { + configured = 1; + if (client->seid == seid) + cap = avdtp_stream_get_codec(stream); + } + + for (cl = clients; cl; cl = cl->next) { + struct unix_client *c = cl->data; + struct a2dp_data *ca2dp = &c->d.a2dp; + + if (ca2dp->session == session && c->seid == seid) { + lock = c->lock; + break; + } + } + + sep = a2dp_get_sep(session, stream); + if (sep && a2dp_sep_get_lock(sep)) + lock = BT_WRITE_LOCK; + + a2dp_append_codec(rsp, cap, seid, type, configured, lock); + } + + unix_ipc_sendmsg(client, &rsp->h); + + return; + +failed: + error("discovery failed"); + unix_ipc_error(client, BT_GET_CAPABILITIES, EIO); + + if (a2dp->sep) { + a2dp_sep_unlock(a2dp->sep, a2dp->session); + a2dp->sep = NULL; + } + + avdtp_unref(a2dp->session); + a2dp->session = NULL; + a2dp->stream = NULL; +} + +static void a2dp_config_complete(struct avdtp *session, struct a2dp_sep *sep, + struct avdtp_stream *stream, + struct avdtp_error *err, + void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_set_configuration_rsp *rsp = (void *) buf; + struct a2dp_data *a2dp = &client->d.a2dp; + uint16_t imtu, omtu; + GSList *caps; + + client->req_id = 0; + + if (err) + goto failed; + + memset(buf, 0, sizeof(buf)); + + if (!stream) + goto failed; + + if (client->cb_id > 0) + avdtp_stream_remove_cb(a2dp->session, a2dp->stream, + client->cb_id); + + a2dp->sep = sep; + a2dp->stream = stream; + + if (!avdtp_stream_get_transport(stream, &client->data_fd, &imtu, &omtu, + &caps)) { + error("Unable to get stream transport"); + goto failed; + } + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_SET_CONFIGURATION; + rsp->h.length = sizeof(*rsp); + + /* FIXME: Use imtu when fd_opt is CFG_FD_OPT_READ */ + rsp->link_mtu = omtu; + + unix_ipc_sendmsg(client, &rsp->h); + + client->cb_id = avdtp_stream_add_cb(session, stream, + stream_state_changed, client); + + return; + +failed: + error("config failed"); + + unix_ipc_error(client, BT_SET_CONFIGURATION, EIO); + + avdtp_unref(a2dp->session); + + a2dp->session = NULL; + a2dp->stream = NULL; + a2dp->sep = NULL; +} + +static void a2dp_resume_complete(struct avdtp *session, + struct avdtp_error *err, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_start_stream_rsp *rsp = (void *) buf; + struct bt_new_stream_ind *ind = (void *) buf; + struct a2dp_data *a2dp = &client->d.a2dp; + + if (err) + goto failed; + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_START_STREAM; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + memset(buf, 0, sizeof(buf)); + ind->h.type = BT_RESPONSE; + ind->h.name = BT_NEW_STREAM; + rsp->h.length = sizeof(*ind); + + unix_ipc_sendmsg(client, &ind->h); + + if (unix_sendmsg_fd(client->sock, client->data_fd) < 0) { + error("unix_sendmsg_fd: %s(%d)", strerror(errno), errno); + goto failed; + } + + return; + +failed: + error("resume failed"); + + unix_ipc_error(client, BT_START_STREAM, EIO); + + if (client->cb_id > 0) { + avdtp_stream_remove_cb(a2dp->session, a2dp->stream, + client->cb_id); + client->cb_id = 0; + } + + if (a2dp->sep) { + a2dp_sep_unlock(a2dp->sep, a2dp->session); + a2dp->sep = NULL; + } + + avdtp_unref(a2dp->session); + a2dp->session = NULL; + a2dp->stream = NULL; +} + +static void a2dp_suspend_complete(struct avdtp *session, + struct avdtp_error *err, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_stop_stream_rsp *rsp = (void *) buf; + + if (err) + goto failed; + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_STOP_STREAM; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + return; + +failed: + error("suspend failed"); + + unix_ipc_error(client, BT_STOP_STREAM, EIO); +} + +static void start_discovery(struct audio_device *dev, struct unix_client *client) +{ + struct a2dp_data *a2dp; + int err = 0; + + switch (client->type) { + case TYPE_SINK: + case TYPE_SOURCE: + a2dp = &client->d.a2dp; + + if (!a2dp->session) + a2dp->session = avdtp_get(&dev->src, &dev->dst); + + if (!a2dp->session) { + error("Unable to get a session"); + goto failed; + } + + err = avdtp_discover(a2dp->session, a2dp_discovery_complete, + client); + if (err) { + if (a2dp->session) { + avdtp_unref(a2dp->session); + a2dp->session = NULL; + } + goto failed; + } + break; + + case TYPE_HEADSET: + case TYPE_GATEWAY: + headset_discovery_complete(dev, client); + break; + + default: + error("No known services for device"); + goto failed; + } + + client->dev = dev; + + return; + +failed: + unix_ipc_error(client, BT_GET_CAPABILITIES, err ? : EIO); +} + +static void open_complete(struct audio_device *dev, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_open_rsp *rsp = (void *) buf; + + memset(buf, 0, sizeof(buf)); + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_OPEN; + rsp->h.length = sizeof(*rsp); + + ba2str(&dev->src, rsp->source); + ba2str(&dev->dst, rsp->destination); + strncpy(rsp->object, dev->path, sizeof(rsp->object)); + + unix_ipc_sendmsg(client, &rsp->h); +} + +static void start_open(struct audio_device *dev, struct unix_client *client) +{ + struct a2dp_data *a2dp; + struct headset_data *hs; + struct avdtp_remote_sep *rsep; + gboolean unref_avdtp_on_fail = FALSE; + + switch (client->type) { + case TYPE_SINK: + case TYPE_SOURCE: + a2dp = &client->d.a2dp; + + if (!a2dp->session) { + a2dp->session = avdtp_get(&dev->src, &dev->dst); + unref_avdtp_on_fail = TRUE; + } + + if (!a2dp->session) { + error("Unable to get a session"); + goto failed; + } + + if (a2dp->sep) { + error("Client already has an opened session"); + goto failed; + } + + rsep = avdtp_get_remote_sep(a2dp->session, client->seid); + if (!rsep) { + error("Invalid seid %d", client->seid); + goto failed; + } + + a2dp->sep = a2dp_get(a2dp->session, rsep); + if (!a2dp->sep) { + error("seid %d not available or locked", client->seid); + goto failed; + } + + if (!a2dp_sep_lock(a2dp->sep, a2dp->session)) { + error("Unable to open seid %d", client->seid); + a2dp->sep = NULL; + goto failed; + } + + break; + + case TYPE_HEADSET: + hs = &client->d.hs; + + if (hs->locked) { + error("Client already has an opened session"); + goto failed; + } + + hs->locked = headset_lock(dev, client->lock); + if (!hs->locked) { + error("Unable to open seid %d", client->seid); + goto failed; + } + break; + + case TYPE_GATEWAY: + break; + default: + error("No known services for device"); + goto failed; + } + + client->dev = dev; + + open_complete(dev, client); + + return; + +failed: + if (unref_avdtp_on_fail && a2dp->session) { + avdtp_unref(a2dp->session); + a2dp->session = NULL; + } + unix_ipc_error(client, BT_OPEN, EINVAL); +} + +static void start_config(struct audio_device *dev, struct unix_client *client) +{ + struct a2dp_data *a2dp; + struct headset_data *hs; + unsigned int id; + + switch (client->type) { + case TYPE_SINK: + case TYPE_SOURCE: + a2dp = &client->d.a2dp; + + if (!a2dp->session) + a2dp->session = avdtp_get(&dev->src, &dev->dst); + + if (!a2dp->session) { + error("Unable to get a session"); + goto failed; + } + + if (!a2dp->sep) { + error("seid %d not opened", client->seid); + goto failed; + } + + id = a2dp_config(a2dp->session, a2dp->sep, a2dp_config_complete, + client->caps, client); + client->cancel = a2dp_cancel; + break; + + case TYPE_HEADSET: + hs = &client->d.hs; + + if (!hs->locked) { + error("seid %d not opened", client->seid); + goto failed; + } + + id = headset_config_stream(dev, TRUE, headset_setup_complete, + client); + client->cancel = headset_cancel_stream; + break; + case TYPE_GATEWAY: + id = gateway_config_stream(dev, gateway_setup_complete, client); + client->cancel = gateway_cancel_stream; + break; + + default: + error("No known services for device"); + goto failed; + } + + if (id == 0) { + error("config failed"); + goto failed; + } + + client->req_id = id; + g_slist_free(client->caps); + client->caps = NULL; + + return; + +failed: + unix_ipc_error(client, BT_SET_CONFIGURATION, EIO); +} + +static void start_resume(struct audio_device *dev, struct unix_client *client) +{ + struct a2dp_data *a2dp = NULL; + struct headset_data *hs; + unsigned int id; + struct avdtp *session = NULL; + + switch (client->type) { + case TYPE_SINK: + case TYPE_SOURCE: + a2dp = &client->d.a2dp; + + if (!a2dp->sep) { + error("seid not opened"); + goto failed; + } + + if (!a2dp->session) { + session = avdtp_get(&dev->src, &dev->dst); + if (!session) { + error("Unable to get a session"); + goto failed; + } + a2dp->session = session; + } + + id = a2dp_resume(a2dp->session, a2dp->sep, a2dp_resume_complete, + client); + client->cancel = a2dp_cancel; + + break; + + case TYPE_HEADSET: + hs = &client->d.hs; + + if (!hs->locked) { + error("seid not opened"); + goto failed; + } + + id = headset_request_stream(dev, headset_resume_complete, + client); + client->cancel = headset_cancel_stream; + break; + + case TYPE_GATEWAY: + id = gateway_request_stream(dev, gateway_resume_complete, + client); + client->cancel = gateway_cancel_stream; + break; + + default: + error("No known services for device"); + goto failed; + } + + if (id == 0) { + error("start_resume: resume failed"); + goto failed; + } + + client->req_id = id; + + return; + +failed: + if (session) { + avdtp_unref(session); + a2dp->session = NULL; + } + + unix_ipc_error(client, BT_START_STREAM, EIO); +} + +static void start_suspend(struct audio_device *dev, struct unix_client *client) +{ + struct a2dp_data *a2dp = NULL; + struct headset_data *hs; + unsigned int id; + struct avdtp *session = NULL; + + switch (client->type) { + case TYPE_SINK: + case TYPE_SOURCE: + a2dp = &client->d.a2dp; + + if (!a2dp->sep) { + error("seid not opened"); + goto failed; + } + + if (!a2dp->session) { + session = avdtp_get(&dev->src, &dev->dst); + if (!session) { + error("Unable to get a session"); + goto failed; + } + a2dp->session = session; + } + + if (!a2dp->sep) { + error("Unable to get a sep"); + goto failed; + } + + id = a2dp_suspend(a2dp->session, a2dp->sep, + a2dp_suspend_complete, client); + client->cancel = a2dp_cancel; + break; + + case TYPE_HEADSET: + hs = &client->d.hs; + + if (!hs->locked) { + error("seid not opened"); + goto failed; + } + + id = headset_suspend_stream(dev, headset_suspend_complete, + client); + client->cancel = headset_cancel_stream; + break; + + case TYPE_GATEWAY: + gateway_suspend_stream(dev); + client->cancel = gateway_cancel_stream; + headset_suspend_complete(dev, client); + id = 1; + break; + + default: + error("No known services for device"); + goto failed; + } + + if (id == 0) { + error("suspend failed"); + goto failed; + } + + return; + +failed: + if (session) { + avdtp_unref(session); + a2dp->session = NULL; + } + + unix_ipc_error(client, BT_STOP_STREAM, EIO); +} + +static void close_complete(struct audio_device *dev, void *user_data) +{ + struct unix_client *client = user_data; + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_close_rsp *rsp = (void *) buf; + + memset(buf, 0, sizeof(buf)); + + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_CLOSE; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + return; +} + +static void start_close(struct audio_device *dev, struct unix_client *client, + gboolean reply) +{ + struct a2dp_data *a2dp; + struct headset_data *hs; + + if (!client->dev) + goto failed; + + switch (client->type) { + case TYPE_HEADSET: + hs = &client->d.hs; + + if (client->dev && hs->locked) { + headset_unlock(client->dev, client->lock); + hs->locked = FALSE; + } + break; + case TYPE_GATEWAY: + break; + case TYPE_SOURCE: + case TYPE_SINK: + a2dp = &client->d.a2dp; + + if (client->cb_id > 0) { + avdtp_stream_remove_cb(a2dp->session, a2dp->stream, + client->cb_id); + client->cb_id = 0; + } + if (a2dp->sep) { + a2dp_sep_unlock(a2dp->sep, a2dp->session); + a2dp->sep = NULL; + } + if (a2dp->session) { + avdtp_unref(a2dp->session); + a2dp->session = NULL; + } + a2dp->stream = NULL; + break; + default: + error("No known services for device"); + goto failed; + } + + if (!reply) + return; + + close_complete(dev, client); + client->dev = NULL; + + return; + +failed: + if (reply) + unix_ipc_error(client, BT_STOP_STREAM, EINVAL); +} + +static void handle_getcapabilities_req(struct unix_client *client, + struct bt_get_capabilities_req *req) +{ + struct audio_device *dev; + bdaddr_t src, dst; + int err = EIO; + const char *interface; + + if (!check_nul(req->source) || !check_nul(req->destination) || + !check_nul(req->object)) { + err = EINVAL; + goto failed; + } + + str2ba(req->source, &src); + str2ba(req->destination, &dst); + + if (!manager_find_device(req->object, &src, &dst, NULL, FALSE)) + goto failed; + + if (req->transport == BT_CAPABILITIES_TRANSPORT_SCO) + interface = AUDIO_HEADSET_INTERFACE; + else if (req->transport == BT_CAPABILITIES_TRANSPORT_A2DP) + interface = AUDIO_SINK_INTERFACE; + else + interface = client->interface; + + dev = manager_find_device(req->object, &src, &dst, interface, TRUE); + if (!dev && (req->flags & BT_FLAG_AUTOCONNECT)) + dev = manager_find_device(req->object, &src, &dst, + interface, FALSE); + + if (!dev) { + if (req->transport == BT_CAPABILITIES_TRANSPORT_SCO) + interface = AUDIO_GATEWAY_INTERFACE; + else if (req->transport == BT_CAPABILITIES_TRANSPORT_A2DP) + interface = AUDIO_SOURCE_INTERFACE; + else + interface = NULL; + dev = manager_find_device(req->object, &src, &dst, + interface, TRUE); + if (!dev && (req->flags & BT_FLAG_AUTOCONNECT)) + dev = manager_find_device(req->object, &src, &dst, + interface, FALSE); + } + + if (!dev) { + error("Unable to find a matching device"); + goto failed; + } + + client->type = select_service(dev, interface); + if (client->type == TYPE_NONE) { + error("No matching service found"); + goto failed; + } + + if (g_strcmp0(interface, client->interface) != 0) { + g_free(client->interface); + client->interface = g_strdup(interface); + } + + client->seid = req->seid; + + start_discovery(dev, client); + + return; + +failed: + unix_ipc_error(client, BT_GET_CAPABILITIES, err); +} + +static int handle_sco_open(struct unix_client *client, struct bt_open_req *req) +{ + if (!client->interface) + client->interface = g_strdup(AUDIO_HEADSET_INTERFACE); + else if (!g_str_equal(client->interface, AUDIO_HEADSET_INTERFACE) && + !g_str_equal(client->interface, AUDIO_GATEWAY_INTERFACE)) + return -EIO; + + DBG("open sco - object=%s source=%s destination=%s lock=%s%s", + strcmp(req->object, "") ? req->object : "ANY", + strcmp(req->source, "") ? req->source : "ANY", + strcmp(req->destination, "") ? req->destination : "ANY", + req->lock & BT_READ_LOCK ? "read" : "", + req->lock & BT_WRITE_LOCK ? "write" : ""); + + return 0; +} + +static int handle_a2dp_open(struct unix_client *client, struct bt_open_req *req) +{ + if (!client->interface) + /* FIXME: are we treating a sink or a source? */ + client->interface = g_strdup(AUDIO_SINK_INTERFACE); + else if (!g_str_equal(client->interface, AUDIO_SINK_INTERFACE) && + !g_str_equal(client->interface, AUDIO_SOURCE_INTERFACE)) + return -EIO; + + DBG("open a2dp - object=%s source=%s destination=%s lock=%s%s", + strcmp(req->object, "") ? req->object : "ANY", + strcmp(req->source, "") ? req->source : "ANY", + strcmp(req->destination, "") ? req->destination : "ANY", + req->lock & BT_READ_LOCK ? "read" : "", + req->lock & BT_WRITE_LOCK ? "write" : ""); + + return 0; +} + +static void handle_open_req(struct unix_client *client, struct bt_open_req *req) +{ + struct audio_device *dev; + bdaddr_t src, dst; + int err = 0; + + if (!check_nul(req->source) || !check_nul(req->destination) || + !check_nul(req->object)) { + err = EINVAL; + goto failed; + } + + str2ba(req->source, &src); + str2ba(req->destination, &dst); + + if (req->seid > BT_A2DP_SEID_RANGE) { + err = handle_sco_open(client, req); + if (err < 0) { + err = -err; + goto failed; + } + } else { + err = handle_a2dp_open(client, req); + if (err < 0) { + err = -err; + goto failed; + } + } + + if (!manager_find_device(req->object, &src, &dst, NULL, FALSE)) + goto failed; + + dev = manager_find_device(req->object, &src, &dst, client->interface, + TRUE); + if (!dev) + dev = manager_find_device(req->object, &src, &dst, + client->interface, FALSE); + + if (!dev) + goto failed; + + client->seid = req->seid; + client->lock = req->lock; + + start_open(dev, client); + + return; + +failed: + unix_ipc_error(client, BT_OPEN, err ? : EIO); +} + +static int handle_sco_transport(struct unix_client *client, + struct bt_set_configuration_req *req) +{ + struct audio_device *dev = client->dev; + + if (!client->interface) { + if (dev->headset) + client->interface = g_strdup(AUDIO_HEADSET_INTERFACE); + else if (dev->gateway) + client->interface = g_strdup(AUDIO_GATEWAY_INTERFACE); + else + return -EIO; + } else if (!g_str_equal(client->interface, AUDIO_HEADSET_INTERFACE) && + !g_str_equal(client->interface, AUDIO_GATEWAY_INTERFACE)) + return -EIO; + + return 0; +} + +static int handle_a2dp_transport(struct unix_client *client, + struct bt_set_configuration_req *req) +{ + struct avdtp_service_capability *media_transport, *media_codec; + struct sbc_codec_cap sbc_cap; + struct mpeg_codec_cap mpeg_cap; + + if (!client->interface) + /* FIXME: are we treating a sink or a source? */ + client->interface = g_strdup(AUDIO_SINK_INTERFACE); + else if (!g_str_equal(client->interface, AUDIO_SINK_INTERFACE) && + !g_str_equal(client->interface, AUDIO_SOURCE_INTERFACE)) + return -EIO; + + g_slist_free_full(client->caps, g_free); + client->caps = NULL; + + media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, + NULL, 0); + + client->caps = g_slist_append(client->caps, media_transport); + + if (req->codec.type == BT_A2DP_MPEG12_SINK || + req->codec.type == BT_A2DP_MPEG12_SOURCE) { + mpeg_capabilities_t *mpeg = (void *) &req->codec; + + memset(&mpeg_cap, 0, sizeof(mpeg_cap)); + + mpeg_cap.cap.media_type = AVDTP_MEDIA_TYPE_AUDIO; + mpeg_cap.cap.media_codec_type = A2DP_CODEC_MPEG12; + mpeg_cap.channel_mode = mpeg->channel_mode; + mpeg_cap.crc = mpeg->crc; + mpeg_cap.layer = mpeg->layer; + mpeg_cap.frequency = mpeg->frequency; + mpeg_cap.mpf = mpeg->mpf; + mpeg_cap.bitrate = mpeg->bitrate; + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, &mpeg_cap, + sizeof(mpeg_cap)); + + print_mpeg12(&mpeg_cap); + } else if (req->codec.type == BT_A2DP_SBC_SINK || + req->codec.type == BT_A2DP_SBC_SOURCE) { + sbc_capabilities_t *sbc = (void *) &req->codec; + + memset(&sbc_cap, 0, sizeof(sbc_cap)); + + sbc_cap.cap.media_type = AVDTP_MEDIA_TYPE_AUDIO; + sbc_cap.cap.media_codec_type = A2DP_CODEC_SBC; + sbc_cap.channel_mode = sbc->channel_mode; + sbc_cap.frequency = sbc->frequency; + sbc_cap.allocation_method = sbc->allocation_method; + sbc_cap.subbands = sbc->subbands; + sbc_cap.block_length = sbc->block_length; + sbc_cap.min_bitpool = sbc->min_bitpool; + sbc_cap.max_bitpool = sbc->max_bitpool; + + media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, &sbc_cap, + sizeof(sbc_cap)); + + print_sbc(&sbc_cap); + } else + return -EINVAL; + + client->caps = g_slist_append(client->caps, media_codec); + + return 0; +} + +static void handle_setconfiguration_req(struct unix_client *client, + struct bt_set_configuration_req *req) +{ + int err = 0; + + if (req->codec.seid != client->seid) { + error("Unable to set configuration: seid %d not opened", + req->codec.seid); + goto failed; + } + + if (!client->dev) + goto failed; + + if (req->codec.transport == BT_CAPABILITIES_TRANSPORT_SCO) { + err = handle_sco_transport(client, req); + if (err < 0) { + err = -err; + goto failed; + } + } else if (req->codec.transport == BT_CAPABILITIES_TRANSPORT_A2DP) { + err = handle_a2dp_transport(client, req); + if (err < 0) { + err = -err; + goto failed; + } + } + + start_config(client->dev, client); + + return; + +failed: + unix_ipc_error(client, BT_SET_CONFIGURATION, err ? : EIO); +} + +static void handle_streamstart_req(struct unix_client *client, + struct bt_start_stream_req *req) +{ + if (!client->dev) + goto failed; + + start_resume(client->dev, client); + + return; + +failed: + unix_ipc_error(client, BT_START_STREAM, EIO); +} + +static void handle_streamstop_req(struct unix_client *client, + struct bt_stop_stream_req *req) +{ + if (!client->dev) + goto failed; + + start_suspend(client->dev, client); + + return; + +failed: + unix_ipc_error(client, BT_STOP_STREAM, EIO); +} + +static void handle_close_req(struct unix_client *client, + struct bt_close_req *req) +{ + if (!client->dev) + goto failed; + + start_close(client->dev, client, TRUE); + + return; + +failed: + unix_ipc_error(client, BT_CLOSE, EIO); +} + +static void handle_control_req(struct unix_client *client, + struct bt_control_req *req) +{ + /* FIXME: really implement that */ + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_set_configuration_rsp *rsp = (void *) buf; + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_CONTROL; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); +} + +static void handle_delay_report_req(struct unix_client *client, + struct bt_delay_report_req *req) +{ + char buf[BT_SUGGESTED_BUFFER_SIZE]; + struct bt_set_configuration_rsp *rsp = (void *) buf; + struct a2dp_data *a2dp; + int err; + + if (!client->dev) { + err = -ENODEV; + goto failed; + } + + switch (client->type) { + case TYPE_HEADSET: + case TYPE_GATEWAY: + err = -EINVAL; + goto failed; + case TYPE_SOURCE: + case TYPE_SINK: + a2dp = &client->d.a2dp; + if (a2dp->session && a2dp->stream) { + err = avdtp_delay_report(a2dp->session, a2dp->stream, + req->delay); + if (err < 0) + goto failed; + } else { + err = -EINVAL; + goto failed; + } + break; + default: + error("No known services for device"); + err = -EINVAL; + goto failed; + } + + memset(buf, 0, sizeof(buf)); + rsp->h.type = BT_RESPONSE; + rsp->h.name = BT_DELAY_REPORT; + rsp->h.length = sizeof(*rsp); + + unix_ipc_sendmsg(client, &rsp->h); + + return; + +failed: + unix_ipc_error(client, BT_DELAY_REPORT, -err); +} + +static gboolean client_cb(GIOChannel *chan, GIOCondition cond, gpointer data) +{ + char buf[BT_SUGGESTED_BUFFER_SIZE]; + bt_audio_msg_header_t *msghdr = (void *) buf; + struct unix_client *client = data; + int len; + const char *type, *name; + + if (cond & G_IO_NVAL) + return FALSE; + + if (cond & (G_IO_HUP | G_IO_ERR)) { + DBG("Unix client disconnected (fd=%d)", client->sock); + + goto failed; + } + + memset(buf, 0, sizeof(buf)); + + len = recv(client->sock, buf, sizeof(buf), 0); + if (len < 0) { + error("recv: %s (%d)", strerror(errno), errno); + goto failed; + } + + type = bt_audio_strtype(msghdr->type); + name = bt_audio_strname(msghdr->name); + + DBG("Audio API: %s <- %s", type, name); + + if (msghdr->length != len) { + error("Invalid message: length mismatch"); + goto failed; + } + + switch (msghdr->name) { + case BT_GET_CAPABILITIES: + handle_getcapabilities_req(client, + (struct bt_get_capabilities_req *) msghdr); + break; + case BT_OPEN: + handle_open_req(client, + (struct bt_open_req *) msghdr); + break; + case BT_SET_CONFIGURATION: + handle_setconfiguration_req(client, + (struct bt_set_configuration_req *) msghdr); + break; + case BT_START_STREAM: + handle_streamstart_req(client, + (struct bt_start_stream_req *) msghdr); + break; + case BT_STOP_STREAM: + handle_streamstop_req(client, + (struct bt_stop_stream_req *) msghdr); + break; + case BT_CLOSE: + handle_close_req(client, + (struct bt_close_req *) msghdr); + break; + case BT_CONTROL: + handle_control_req(client, + (struct bt_control_req *) msghdr); + break; + case BT_DELAY_REPORT: + handle_delay_report_req(client, + (struct bt_delay_report_req *) msghdr); + break; + default: + error("Audio API: received unexpected message name %d", + msghdr->name); + } + + return TRUE; + +failed: + clients = g_slist_remove(clients, client); + start_close(client->dev, client, FALSE); + client_free(client); + return FALSE; +} + +static gboolean server_cb(GIOChannel *chan, GIOCondition cond, gpointer data) +{ + struct sockaddr_un addr; + socklen_t addrlen; + int sk, cli_sk; + struct unix_client *client; + GIOChannel *io; + + if (cond & G_IO_NVAL) + return FALSE; + + if (cond & (G_IO_HUP | G_IO_ERR)) { + g_io_channel_shutdown(chan, TRUE, NULL); + return FALSE; + } + + sk = g_io_channel_unix_get_fd(chan); + + memset(&addr, 0, sizeof(addr)); + addrlen = sizeof(addr); + + cli_sk = accept(sk, (struct sockaddr *) &addr, &addrlen); + if (cli_sk < 0) { + error("accept: %s (%d)", strerror(errno), errno); + return TRUE; + } + + DBG("Accepted new client connection on unix socket (fd=%d)", cli_sk); + set_nonblocking(cli_sk); + + client = g_new0(struct unix_client, 1); + client->sock = cli_sk; + clients = g_slist_append(clients, client); + + io = g_io_channel_unix_new(cli_sk); + g_io_add_watch(io, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, + client_cb, client); + g_io_channel_unref(io); + + return TRUE; +} + +void unix_device_removed(struct audio_device *dev) +{ + GSList *l; + + DBG("unix_device_removed(%p)", dev); + + l = clients; + while (l) { + struct unix_client *client = l->data; + + l = l->next; + + if (client->dev == dev) { + clients = g_slist_remove(clients, client); + start_close(client->dev, client, FALSE); + client_free(client); + } + } +} + +void unix_delay_report(struct audio_device *dev, uint8_t seid, uint16_t delay) +{ + GSList *l; + struct bt_delay_report_ind ind; + + DBG("unix_delay_report(%p): %u.%ums", dev, delay / 10, delay % 10); + + memset(&ind, 0, sizeof(ind)); + ind.h.type = BT_INDICATION; + ind.h.name = BT_DELAY_REPORT; + ind.h.length = sizeof(ind); + ind.delay = delay; + + for (l = clients; l != NULL; l = g_slist_next(l)) { + struct unix_client *client = l->data; + + if (client->dev != dev || client->seid != seid) + continue; + + unix_ipc_sendmsg(client, (void *) &ind); + } +} + +int unix_init(void) +{ + GIOChannel *io; + struct sockaddr_un addr = { + AF_UNIX, BT_IPC_SOCKET_NAME + }; + + int sk, err; + + sk = socket(PF_LOCAL, SOCK_STREAM, 0); + if (sk < 0) { + err = -errno; + error("Can't create unix socket: %s (%d)", strerror(-err), + -err); + return err; + } + + if (bind(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + err = -errno; + error("Can't bind unix socket: %s (%d)", strerror(-err), + -err); + close(sk); + return err; + } + + set_nonblocking(sk); + + if (listen(sk, 1) < 0) { + err = -errno; + error("Can't listen on unix socket: %s (%d)", strerror(-err), + -err); + close(sk); + return err; + } + + unix_sock = sk; + + io = g_io_channel_unix_new(sk); + g_io_add_watch(io, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, + server_cb, NULL); + g_io_channel_unref(io); + + DBG("Unix socket created: %d", sk); + + return 0; +} + +void unix_exit(void) +{ + g_slist_free_full(clients, client_free); + if (unix_sock >= 0) { + close(unix_sock); + unix_sock = -1; + } +} diff --git a/audio/unix.h b/audio/unix.h new file mode 100644 index 0000000..74ca16d --- /dev/null +++ b/audio/unix.h @@ -0,0 +1,30 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2006-2010 Nokia Corporation + * Copyright (C) 2004-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +void unix_device_removed(struct audio_device *dev); + +void unix_delay_report(struct audio_device *dev, uint8_t seid, uint16_t delay); + +int unix_init(void); +void unix_exit(void); diff --git a/bluez.manifest b/bluez.manifest new file mode 100644 index 0000000..e4290cb --- /dev/null +++ b/bluez.manifest @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bluez.pc.in b/bluez.pc.in new file mode 100644 index 0000000..3d6e596 --- /dev/null +++ b/bluez.pc.in @@ -0,0 +1,10 @@ +prefix=@prefix@ +exec_prefix=@exec_prefix@ +libdir=@libdir@ +includedir=@includedir@ + +Name: BlueZ +Description: Bluetooth protocol stack for Linux +Version: @VERSION@ +Libs: -L${libdir} -lbluetooth +Cflags: -I${includedir} diff --git a/btio/btio.c b/btio/btio.c new file mode 100644 index 0000000..e81fb75 --- /dev/null +++ b/btio/btio.c @@ -0,0 +1,1460 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2009-2010 Marcel Holtmann + * Copyright (C) 2009-2010 Nokia Corporation + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "btio.h" + +#ifndef BT_FLUSHABLE +#define BT_FLUSHABLE 8 +#endif + +#define ERROR_FAILED(gerr, str, err) \ + g_set_error(gerr, BT_IO_ERROR, BT_IO_ERROR_FAILED, \ + str ": %s (%d)", strerror(err), err) + +#define DEFAULT_DEFER_TIMEOUT 30 + +struct set_opts { + bdaddr_t src; + bdaddr_t dst; + uint8_t dst_type; + int defer; + int sec_level; + uint8_t channel; + uint16_t psm; + uint16_t cid; + uint16_t mtu; + uint16_t imtu; + uint16_t omtu; + int master; + uint8_t mode; + int flushable; + uint32_t priority; +}; + +struct connect { + BtIOConnect connect; + gpointer user_data; + GDestroyNotify destroy; +}; + +struct accept { + BtIOConnect connect; + gpointer user_data; + GDestroyNotify destroy; +}; + +struct server { + BtIOConnect connect; + BtIOConfirm confirm; + gpointer user_data; + GDestroyNotify destroy; +}; + +static void server_remove(struct server *server) +{ + if (server->destroy) + server->destroy(server->user_data); + g_free(server); +} + +static void connect_remove(struct connect *conn) +{ + if (conn->destroy) + conn->destroy(conn->user_data); + g_free(conn); +} + +static void accept_remove(struct accept *accept) +{ + if (accept->destroy) + accept->destroy(accept->user_data); + g_free(accept); +} + +static gboolean check_nval(GIOChannel *io) +{ + struct pollfd fds; + + memset(&fds, 0, sizeof(fds)); + fds.fd = g_io_channel_unix_get_fd(io); + fds.events = POLLNVAL; + + if (poll(&fds, 1, 0) > 0 && (fds.revents & POLLNVAL)) + return TRUE; + + return FALSE; +} + +static gboolean accept_cb(GIOChannel *io, GIOCondition cond, + gpointer user_data) +{ + struct accept *accept = user_data; + GError *err = NULL; + + /* If the user aborted this accept attempt */ + if ((cond & G_IO_NVAL) || check_nval(io)) + return FALSE; + + if (cond & (G_IO_HUP | G_IO_ERR)) + g_set_error(&err, BT_IO_ERROR, BT_IO_ERROR_DISCONNECTED, + "HUP or ERR on socket"); + + accept->connect(io, err, accept->user_data); + + g_clear_error(&err); + + return FALSE; +} + +static gboolean connect_cb(GIOChannel *io, GIOCondition cond, + gpointer user_data) +{ + struct connect *conn = user_data; + GError *gerr = NULL; + + /* If the user aborted this connect attempt */ + if ((cond & G_IO_NVAL) || check_nval(io)) + return FALSE; + + if (cond & G_IO_OUT) { + int err, sk_err = 0, sock = g_io_channel_unix_get_fd(io); + socklen_t len = sizeof(sk_err); + + if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &sk_err, &len) < 0) + err = -errno; + else + err = -sk_err; + + if (err < 0) + g_set_error(&gerr, BT_IO_ERROR, + BT_IO_ERROR_CONNECT_FAILED, "%s (%d)", + strerror(-err), -err); + } else if (cond & (G_IO_HUP | G_IO_ERR)) + g_set_error(&gerr, BT_IO_ERROR, BT_IO_ERROR_CONNECT_FAILED, + "HUP or ERR on socket"); + + conn->connect(io, gerr, conn->user_data); + + if (gerr) + g_error_free(gerr); + + return FALSE; +} + +static gboolean server_cb(GIOChannel *io, GIOCondition cond, + gpointer user_data) +{ + struct server *server = user_data; + int srv_sock, cli_sock; + GIOChannel *cli_io; + + /* If the user closed the server */ + if ((cond & G_IO_NVAL) || check_nval(io)) + return FALSE; + + srv_sock = g_io_channel_unix_get_fd(io); + + cli_sock = accept(srv_sock, NULL, NULL); + if (cli_sock < 0) + return TRUE; + + cli_io = g_io_channel_unix_new(cli_sock); + + g_io_channel_set_close_on_unref(cli_io, TRUE); + g_io_channel_set_flags(cli_io, G_IO_FLAG_NONBLOCK, NULL); + + if (server->confirm) + server->confirm(cli_io, server->user_data); + else + server->connect(cli_io, NULL, server->user_data); + + g_io_channel_unref(cli_io); + + return TRUE; +} + +static void server_add(GIOChannel *io, BtIOConnect connect, + BtIOConfirm confirm, gpointer user_data, + GDestroyNotify destroy) +{ + struct server *server; + GIOCondition cond; + + server = g_new0(struct server, 1); + server->connect = connect; + server->confirm = confirm; + server->user_data = user_data; + server->destroy = destroy; + + cond = G_IO_IN | G_IO_ERR | G_IO_HUP | G_IO_NVAL; + g_io_add_watch_full(io, G_PRIORITY_DEFAULT, cond, server_cb, server, + (GDestroyNotify) server_remove); +} + +static void connect_add(GIOChannel *io, BtIOConnect connect, + gpointer user_data, GDestroyNotify destroy) +{ + struct connect *conn; + GIOCondition cond; + + conn = g_new0(struct connect, 1); + conn->connect = connect; + conn->user_data = user_data; + conn->destroy = destroy; + + cond = G_IO_OUT | G_IO_ERR | G_IO_HUP | G_IO_NVAL; + g_io_add_watch_full(io, G_PRIORITY_DEFAULT, cond, connect_cb, conn, + (GDestroyNotify) connect_remove); +} + +static void accept_add(GIOChannel *io, BtIOConnect connect, gpointer user_data, + GDestroyNotify destroy) +{ + struct accept *accept; + GIOCondition cond; + + accept = g_new0(struct accept, 1); + accept->connect = connect; + accept->user_data = user_data; + accept->destroy = destroy; + + cond = G_IO_OUT | G_IO_ERR | G_IO_HUP | G_IO_NVAL; + g_io_add_watch_full(io, G_PRIORITY_DEFAULT, cond, accept_cb, accept, + (GDestroyNotify) accept_remove); +} + +static int l2cap_bind(int sock, const bdaddr_t *src, uint16_t psm, + uint16_t cid, GError **err) +{ + struct sockaddr_l2 addr; + + memset(&addr, 0, sizeof(addr)); + addr.l2_family = AF_BLUETOOTH; + bacpy(&addr.l2_bdaddr, src); + + if (cid) + addr.l2_cid = htobs(cid); + else + addr.l2_psm = htobs(psm); + + if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + int error = -errno; + ERROR_FAILED(err, "l2cap_bind", errno); + return error; + } + + return 0; +} + +static int l2cap_connect(int sock, const bdaddr_t *dst, uint8_t dst_type, + uint16_t psm, uint16_t cid) +{ + int err; + struct sockaddr_l2 addr; + + memset(&addr, 0, sizeof(addr)); + addr.l2_family = AF_BLUETOOTH; + bacpy(&addr.l2_bdaddr, dst); + if (cid) + addr.l2_cid = htobs(cid); + else + addr.l2_psm = htobs(psm); + + addr.l2_bdaddr_type = dst_type; + + err = connect(sock, (struct sockaddr *) &addr, sizeof(addr)); + if (err < 0 && !(errno == EAGAIN || errno == EINPROGRESS)) + return -errno; + + return 0; +} + +static int l2cap_set_master(int sock, int master) +{ + int flags; + socklen_t len; + + len = sizeof(flags); + if (getsockopt(sock, SOL_L2CAP, L2CAP_LM, &flags, &len) < 0) + return -errno; + + if (master) { + if (flags & L2CAP_LM_MASTER) + return 0; + flags |= L2CAP_LM_MASTER; + } else { + if (!(flags & L2CAP_LM_MASTER)) + return 0; + flags &= ~L2CAP_LM_MASTER; + } + + if (setsockopt(sock, SOL_L2CAP, L2CAP_LM, &flags, sizeof(flags)) < 0) + return -errno; + + return 0; +} + +static int rfcomm_set_master(int sock, int master) +{ + int flags; + socklen_t len; + + len = sizeof(flags); + if (getsockopt(sock, SOL_RFCOMM, RFCOMM_LM, &flags, &len) < 0) + return -errno; + + if (master) { + if (flags & RFCOMM_LM_MASTER) + return 0; + flags |= RFCOMM_LM_MASTER; + } else { + if (!(flags & RFCOMM_LM_MASTER)) + return 0; + flags &= ~RFCOMM_LM_MASTER; + } + + if (setsockopt(sock, SOL_RFCOMM, RFCOMM_LM, &flags, sizeof(flags)) < 0) + return -errno; + + return 0; +} + +static int l2cap_set_lm(int sock, int level) +{ + int lm_map[] = { + 0, + L2CAP_LM_AUTH, + L2CAP_LM_AUTH | L2CAP_LM_ENCRYPT, + L2CAP_LM_AUTH | L2CAP_LM_ENCRYPT | L2CAP_LM_SECURE, + }, opt = lm_map[level]; + + if (setsockopt(sock, SOL_L2CAP, L2CAP_LM, &opt, sizeof(opt)) < 0) + return -errno; + + return 0; +} + +static int rfcomm_set_lm(int sock, int level) +{ + int lm_map[] = { + 0, + RFCOMM_LM_AUTH, + RFCOMM_LM_AUTH | RFCOMM_LM_ENCRYPT, + RFCOMM_LM_AUTH | RFCOMM_LM_ENCRYPT | RFCOMM_LM_SECURE, + }, opt = lm_map[level]; + + if (setsockopt(sock, SOL_RFCOMM, RFCOMM_LM, &opt, sizeof(opt)) < 0) + return -errno; + + return 0; +} + +static gboolean set_sec_level(int sock, BtIOType type, int level, GError **err) +{ + struct bt_security sec; + int ret; + + if (level < BT_SECURITY_LOW || level > BT_SECURITY_HIGH) { + g_set_error(err, BT_IO_ERROR, BT_IO_ERROR_INVALID_ARGS, + "Valid security level range is %d-%d", + BT_SECURITY_LOW, BT_SECURITY_HIGH); + return FALSE; + } + + memset(&sec, 0, sizeof(sec)); + sec.level = level; + + if (setsockopt(sock, SOL_BLUETOOTH, BT_SECURITY, &sec, + sizeof(sec)) == 0) + return TRUE; + + if (errno != ENOPROTOOPT) { + ERROR_FAILED(err, "setsockopt(BT_SECURITY)", errno); + return FALSE; + } + + if (type == BT_IO_L2CAP) + ret = l2cap_set_lm(sock, level); + else + ret = rfcomm_set_lm(sock, level); + + if (ret < 0) { + ERROR_FAILED(err, "setsockopt(LM)", -ret); + return FALSE; + } + + return TRUE; +} + +static int l2cap_get_lm(int sock, int *sec_level) +{ + int opt; + socklen_t len; + + len = sizeof(opt); + if (getsockopt(sock, SOL_L2CAP, L2CAP_LM, &opt, &len) < 0) + return -errno; + + *sec_level = 0; + + if (opt & L2CAP_LM_AUTH) + *sec_level = BT_SECURITY_LOW; + if (opt & L2CAP_LM_ENCRYPT) + *sec_level = BT_SECURITY_MEDIUM; + if (opt & L2CAP_LM_SECURE) + *sec_level = BT_SECURITY_HIGH; + + return 0; +} + +static int rfcomm_get_lm(int sock, int *sec_level) +{ + int opt; + socklen_t len; + + len = sizeof(opt); + if (getsockopt(sock, SOL_RFCOMM, RFCOMM_LM, &opt, &len) < 0) + return -errno; + + *sec_level = 0; + + if (opt & RFCOMM_LM_AUTH) + *sec_level = BT_SECURITY_LOW; + if (opt & RFCOMM_LM_ENCRYPT) + *sec_level = BT_SECURITY_MEDIUM; + if (opt & RFCOMM_LM_SECURE) + *sec_level = BT_SECURITY_HIGH; + + return 0; +} + +static gboolean get_sec_level(int sock, BtIOType type, int *level, + GError **err) +{ + struct bt_security sec; + socklen_t len; + int ret; + + memset(&sec, 0, sizeof(sec)); + len = sizeof(sec); + if (getsockopt(sock, SOL_BLUETOOTH, BT_SECURITY, &sec, &len) == 0) { + *level = sec.level; + return TRUE; + } + + if (errno != ENOPROTOOPT) { + ERROR_FAILED(err, "getsockopt(BT_SECURITY)", errno); + return FALSE; + } + + if (type == BT_IO_L2CAP) + ret = l2cap_get_lm(sock, level); + else + ret = rfcomm_get_lm(sock, level); + + if (ret < 0) { + ERROR_FAILED(err, "getsockopt(LM)", -ret); + return FALSE; + } + + return TRUE; +} + +static int l2cap_set_flushable(int sock, gboolean flushable) +{ + int f; + + f = flushable; + if (setsockopt(sock, SOL_BLUETOOTH, BT_FLUSHABLE, &f, sizeof(f)) < 0) + return -errno; + + return 0; +} + +static int set_priority(int sock, uint32_t prio) +{ + if (setsockopt(sock, SOL_SOCKET, SO_PRIORITY, &prio, sizeof(prio)) < 0) + return -errno; + + return 0; +} + +static gboolean get_key_size(int sock, int *size, GError **err) +{ + struct bt_security sec; + socklen_t len; + + memset(&sec, 0, sizeof(sec)); + len = sizeof(sec); + if (getsockopt(sock, SOL_BLUETOOTH, BT_SECURITY, &sec, &len) == 0) { + *size = sec.key_size; + return TRUE; + } + + return FALSE; +} + +static gboolean l2cap_set(int sock, int sec_level, uint16_t imtu, + uint16_t omtu, uint8_t mode, int master, + int flushable, uint32_t priority, GError **err) +{ + if (imtu || omtu || mode) { + struct l2cap_options l2o; + socklen_t len; + + memset(&l2o, 0, sizeof(l2o)); + len = sizeof(l2o); + if (getsockopt(sock, SOL_L2CAP, L2CAP_OPTIONS, &l2o, + &len) < 0) { + ERROR_FAILED(err, "getsockopt(L2CAP_OPTIONS)", errno); + return FALSE; + } + + if (imtu) + l2o.imtu = imtu; + if (omtu) + l2o.omtu = omtu; + if (mode) + l2o.mode = mode; + + if (setsockopt(sock, SOL_L2CAP, L2CAP_OPTIONS, &l2o, + sizeof(l2o)) < 0) { + ERROR_FAILED(err, "setsockopt(L2CAP_OPTIONS)", errno); + return FALSE; + } + } + + if (master >= 0 && l2cap_set_master(sock, master) < 0) { + ERROR_FAILED(err, "l2cap_set_master", errno); + return FALSE; + } + + if (flushable >= 0 && l2cap_set_flushable(sock, flushable) < 0) { + ERROR_FAILED(err, "l2cap_set_flushable", errno); + return FALSE; + } + + if (priority > 0 && set_priority(sock, priority) < 0) { + ERROR_FAILED(err, "set_priority", errno); + return FALSE; + } + + if (sec_level && !set_sec_level(sock, BT_IO_L2CAP, sec_level, err)) + return FALSE; + + return TRUE; +} + +static int rfcomm_bind(int sock, + const bdaddr_t *src, uint8_t channel, GError **err) +{ + struct sockaddr_rc addr; + + memset(&addr, 0, sizeof(addr)); + addr.rc_family = AF_BLUETOOTH; + bacpy(&addr.rc_bdaddr, src); + addr.rc_channel = channel; + + if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + int error = -errno; + ERROR_FAILED(err, "rfcomm_bind", errno); + return error; + } + + return 0; +} + +static int rfcomm_connect(int sock, const bdaddr_t *dst, uint8_t channel) +{ + int err; + struct sockaddr_rc addr; + + memset(&addr, 0, sizeof(addr)); + addr.rc_family = AF_BLUETOOTH; + bacpy(&addr.rc_bdaddr, dst); + addr.rc_channel = channel; + + err = connect(sock, (struct sockaddr *) &addr, sizeof(addr)); + if (err < 0 && !(errno == EAGAIN || errno == EINPROGRESS)) + return -errno; + + return 0; +} + +static gboolean rfcomm_set(int sock, int sec_level, int master, GError **err) +{ + if (sec_level && !set_sec_level(sock, BT_IO_RFCOMM, sec_level, err)) + return FALSE; + + if (master >= 0 && rfcomm_set_master(sock, master) < 0) { + ERROR_FAILED(err, "rfcomm_set_master", errno); + return FALSE; + } + + return TRUE; +} + +static int sco_bind(int sock, const bdaddr_t *src, GError **err) +{ + struct sockaddr_sco addr; + + memset(&addr, 0, sizeof(addr)); + addr.sco_family = AF_BLUETOOTH; + bacpy(&addr.sco_bdaddr, src); + + if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + int error = -errno; + ERROR_FAILED(err, "sco_bind", errno); + return error; + } + + return 0; +} + +static int sco_connect(int sock, const bdaddr_t *dst) +{ + struct sockaddr_sco addr; + int err; + + memset(&addr, 0, sizeof(addr)); + addr.sco_family = AF_BLUETOOTH; + bacpy(&addr.sco_bdaddr, dst); + + err = connect(sock, (struct sockaddr *) &addr, sizeof(addr)); + if (err < 0 && !(errno == EAGAIN || errno == EINPROGRESS)) + return -errno; + + return 0; +} + +static gboolean sco_set(int sock, uint16_t mtu, GError **err) +{ + struct sco_options sco_opt; + socklen_t len; + + if (!mtu) + return TRUE; + + len = sizeof(sco_opt); + memset(&sco_opt, 0, len); + if (getsockopt(sock, SOL_SCO, SCO_OPTIONS, &sco_opt, &len) < 0) { + ERROR_FAILED(err, "getsockopt(SCO_OPTIONS)", errno); + return FALSE; + } + + sco_opt.mtu = mtu; + if (setsockopt(sock, SOL_SCO, SCO_OPTIONS, &sco_opt, + sizeof(sco_opt)) < 0) { + ERROR_FAILED(err, "setsockopt(SCO_OPTIONS)", errno); + return FALSE; + } + + return TRUE; +} + +static gboolean parse_set_opts(struct set_opts *opts, GError **err, + BtIOOption opt1, va_list args) +{ + BtIOOption opt = opt1; + const char *str; + + memset(opts, 0, sizeof(*opts)); + + /* Set defaults */ + opts->defer = DEFAULT_DEFER_TIMEOUT; + opts->master = -1; + opts->mode = L2CAP_MODE_BASIC; + opts->flushable = -1; + opts->priority = 0; + opts->dst_type = BDADDR_BREDR; + + while (opt != BT_IO_OPT_INVALID) { + switch (opt) { + case BT_IO_OPT_SOURCE: + str = va_arg(args, const char *); + str2ba(str, &opts->src); + break; + case BT_IO_OPT_SOURCE_BDADDR: + bacpy(&opts->src, va_arg(args, const bdaddr_t *)); + break; + case BT_IO_OPT_DEST: + str2ba(va_arg(args, const char *), &opts->dst); + break; + case BT_IO_OPT_DEST_BDADDR: + bacpy(&opts->dst, va_arg(args, const bdaddr_t *)); + break; + case BT_IO_OPT_DEST_TYPE: + opts->dst_type = va_arg(args, int); + break; + case BT_IO_OPT_DEFER_TIMEOUT: + opts->defer = va_arg(args, int); + break; + case BT_IO_OPT_SEC_LEVEL: + opts->sec_level = va_arg(args, int); + break; + case BT_IO_OPT_CHANNEL: + opts->channel = va_arg(args, int); + break; + case BT_IO_OPT_PSM: + opts->psm = va_arg(args, int); + break; + case BT_IO_OPT_CID: + opts->cid = va_arg(args, int); + break; + case BT_IO_OPT_MTU: + opts->mtu = va_arg(args, int); + opts->imtu = opts->mtu; + opts->omtu = opts->mtu; + break; + case BT_IO_OPT_OMTU: + opts->omtu = va_arg(args, int); + if (!opts->mtu) + opts->mtu = opts->omtu; + break; + case BT_IO_OPT_IMTU: + opts->imtu = va_arg(args, int); + if (!opts->mtu) + opts->mtu = opts->imtu; + break; + case BT_IO_OPT_MASTER: + opts->master = va_arg(args, gboolean); + break; + case BT_IO_OPT_MODE: + opts->mode = va_arg(args, int); + break; + case BT_IO_OPT_FLUSHABLE: + opts->flushable = va_arg(args, gboolean); + break; + case BT_IO_OPT_PRIORITY: + opts->priority = va_arg(args, int); + break; + default: + g_set_error(err, BT_IO_ERROR, BT_IO_ERROR_INVALID_ARGS, + "Unknown option %d", opt); + return FALSE; + } + + opt = va_arg(args, int); + } + + return TRUE; +} + +static gboolean get_peers(int sock, struct sockaddr *src, struct sockaddr *dst, + socklen_t len, GError **err) +{ + socklen_t olen; + + memset(src, 0, len); + olen = len; + if (getsockname(sock, src, &olen) < 0) { + ERROR_FAILED(err, "getsockname", errno); + return FALSE; + } + + memset(dst, 0, len); + olen = len; + if (getpeername(sock, dst, &olen) < 0) { + ERROR_FAILED(err, "getpeername", errno); + return FALSE; + } + + return TRUE; +} + +static int l2cap_get_info(int sock, uint16_t *handle, uint8_t *dev_class) +{ + struct l2cap_conninfo info; + socklen_t len; + + len = sizeof(info); + if (getsockopt(sock, SOL_L2CAP, L2CAP_CONNINFO, &info, &len) < 0) + return -errno; + + if (handle) + *handle = info.hci_handle; + + if (dev_class) + memcpy(dev_class, info.dev_class, 3); + + return 0; +} + +static int l2cap_get_flushable(int sock, gboolean *flushable) +{ + int f; + socklen_t len; + + f = 0; + len = sizeof(f); + if (getsockopt(sock, SOL_BLUETOOTH, BT_FLUSHABLE, &f, &len) < 0) + return -errno; + + if (f) + *flushable = TRUE; + else + *flushable = FALSE; + + return 0; +} + +static int get_priority(int sock, uint32_t *prio) +{ + socklen_t len; + + len = sizeof(*prio); + if (getsockopt(sock, SOL_SOCKET, SO_PRIORITY, prio, &len) < 0) + return -errno; + + return 0; +} + +static gboolean l2cap_get(int sock, GError **err, BtIOOption opt1, + va_list args) +{ + BtIOOption opt = opt1; + struct sockaddr_l2 src, dst; + struct l2cap_options l2o; + int flags; + uint8_t dev_class[3]; + uint16_t handle; + socklen_t len; + gboolean flushable = FALSE; + uint32_t priority; + + len = sizeof(l2o); + memset(&l2o, 0, len); + if (getsockopt(sock, SOL_L2CAP, L2CAP_OPTIONS, &l2o, &len) < 0) { + ERROR_FAILED(err, "getsockopt(L2CAP_OPTIONS)", errno); + return FALSE; + } + + if (!get_peers(sock, (struct sockaddr *) &src, + (struct sockaddr *) &dst, sizeof(src), err)) + return FALSE; + + while (opt != BT_IO_OPT_INVALID) { + switch (opt) { + case BT_IO_OPT_SOURCE: + ba2str(&src.l2_bdaddr, va_arg(args, char *)); + break; + case BT_IO_OPT_SOURCE_BDADDR: + bacpy(va_arg(args, bdaddr_t *), &src.l2_bdaddr); + break; + case BT_IO_OPT_DEST: + ba2str(&dst.l2_bdaddr, va_arg(args, char *)); + break; + case BT_IO_OPT_DEST_BDADDR: + bacpy(va_arg(args, bdaddr_t *), &dst.l2_bdaddr); + break; + case BT_IO_OPT_DEST_TYPE: + g_set_error(err, BT_IO_ERROR, BT_IO_ERROR_INVALID_ARGS, + "Not implemented"); + return FALSE; + case BT_IO_OPT_DEFER_TIMEOUT: + len = sizeof(int); + if (getsockopt(sock, SOL_BLUETOOTH, BT_DEFER_SETUP, + va_arg(args, int *), &len) < 0) { + ERROR_FAILED(err, "getsockopt(DEFER_SETUP)", + errno); + return FALSE; + } + break; + case BT_IO_OPT_SEC_LEVEL: + if (!get_sec_level(sock, BT_IO_L2CAP, + va_arg(args, int *), err)) + return FALSE; + break; + case BT_IO_OPT_KEY_SIZE: + if (!get_key_size(sock, va_arg(args, int *), err)) + return FALSE; + break; + case BT_IO_OPT_PSM: + *(va_arg(args, uint16_t *)) = src.l2_psm ? + btohs(src.l2_psm) : btohs(dst.l2_psm); + break; + case BT_IO_OPT_CID: + *(va_arg(args, uint16_t *)) = src.l2_cid ? + btohs(src.l2_cid) : btohs(dst.l2_cid); + break; + case BT_IO_OPT_OMTU: + *(va_arg(args, uint16_t *)) = l2o.omtu; + break; + case BT_IO_OPT_IMTU: + *(va_arg(args, uint16_t *)) = l2o.imtu; + break; + case BT_IO_OPT_MASTER: + len = sizeof(flags); + if (getsockopt(sock, SOL_L2CAP, L2CAP_LM, &flags, + &len) < 0) { + ERROR_FAILED(err, "getsockopt(L2CAP_LM)", + errno); + return FALSE; + } + *(va_arg(args, gboolean *)) = + (flags & L2CAP_LM_MASTER) ? TRUE : FALSE; + break; + case BT_IO_OPT_HANDLE: + if (l2cap_get_info(sock, &handle, dev_class) < 0) { + ERROR_FAILED(err, "L2CAP_CONNINFO", errno); + return FALSE; + } + *(va_arg(args, uint16_t *)) = handle; + break; + case BT_IO_OPT_CLASS: + if (l2cap_get_info(sock, &handle, dev_class) < 0) { + ERROR_FAILED(err, "L2CAP_CONNINFO", errno); + return FALSE; + } + memcpy(va_arg(args, uint8_t *), dev_class, 3); + break; + case BT_IO_OPT_MODE: + *(va_arg(args, uint8_t *)) = l2o.mode; + break; + case BT_IO_OPT_FLUSHABLE: + if (l2cap_get_flushable(sock, &flushable) < 0) { + ERROR_FAILED(err, "get_flushable", errno); + return FALSE; + } + *(va_arg(args, gboolean *)) = flushable; + break; + case BT_IO_OPT_PRIORITY: + if (get_priority(sock, &priority) < 0) { + ERROR_FAILED(err, "get_priority", errno); + return FALSE; + } + *(va_arg(args, uint32_t *)) = priority; + break; + default: + g_set_error(err, BT_IO_ERROR, BT_IO_ERROR_INVALID_ARGS, + "Unknown option %d", opt); + return FALSE; + } + + opt = va_arg(args, int); + } + + return TRUE; +} + +static int rfcomm_get_info(int sock, uint16_t *handle, uint8_t *dev_class) +{ + struct rfcomm_conninfo info; + socklen_t len; + + len = sizeof(info); + if (getsockopt(sock, SOL_RFCOMM, RFCOMM_CONNINFO, &info, &len) < 0) + return -errno; + + if (handle) + *handle = info.hci_handle; + + if (dev_class) + memcpy(dev_class, info.dev_class, 3); + + return 0; +} + +static gboolean rfcomm_get(int sock, GError **err, BtIOOption opt1, + va_list args) +{ + BtIOOption opt = opt1; + struct sockaddr_rc src, dst; + int flags; + socklen_t len; + uint8_t dev_class[3]; + uint16_t handle; + + if (!get_peers(sock, (struct sockaddr *) &src, + (struct sockaddr *) &dst, sizeof(src), err)) + return FALSE; + + while (opt != BT_IO_OPT_INVALID) { + switch (opt) { + case BT_IO_OPT_SOURCE: + ba2str(&src.rc_bdaddr, va_arg(args, char *)); + break; + case BT_IO_OPT_SOURCE_BDADDR: + bacpy(va_arg(args, bdaddr_t *), &src.rc_bdaddr); + break; + case BT_IO_OPT_DEST: + ba2str(&dst.rc_bdaddr, va_arg(args, char *)); + break; + case BT_IO_OPT_DEST_BDADDR: + bacpy(va_arg(args, bdaddr_t *), &dst.rc_bdaddr); + break; + case BT_IO_OPT_DEFER_TIMEOUT: + len = sizeof(int); + if (getsockopt(sock, SOL_BLUETOOTH, BT_DEFER_SETUP, + va_arg(args, int *), &len) < 0) { + ERROR_FAILED(err, "getsockopt(DEFER_SETUP)", + errno); + return FALSE; + } + break; + case BT_IO_OPT_SEC_LEVEL: + if (!get_sec_level(sock, BT_IO_RFCOMM, + va_arg(args, int *), err)) + return FALSE; + break; + case BT_IO_OPT_CHANNEL: + *(va_arg(args, uint8_t *)) = src.rc_channel ? + src.rc_channel : dst.rc_channel; + break; + case BT_IO_OPT_SOURCE_CHANNEL: + *(va_arg(args, uint8_t *)) = src.rc_channel; + break; + case BT_IO_OPT_DEST_CHANNEL: + *(va_arg(args, uint8_t *)) = dst.rc_channel; + break; + case BT_IO_OPT_MASTER: + len = sizeof(flags); + if (getsockopt(sock, SOL_RFCOMM, RFCOMM_LM, &flags, + &len) < 0) { + ERROR_FAILED(err, "getsockopt(RFCOMM_LM)", + errno); + return FALSE; + } + *(va_arg(args, gboolean *)) = + (flags & RFCOMM_LM_MASTER) ? TRUE : FALSE; + break; + case BT_IO_OPT_HANDLE: + if (rfcomm_get_info(sock, &handle, dev_class) < 0) { + ERROR_FAILED(err, "RFCOMM_CONNINFO", errno); + return FALSE; + } + *(va_arg(args, uint16_t *)) = handle; + break; + case BT_IO_OPT_CLASS: + if (rfcomm_get_info(sock, &handle, dev_class) < 0) { + ERROR_FAILED(err, "RFCOMM_CONNINFO", errno); + return FALSE; + } + memcpy(va_arg(args, uint8_t *), dev_class, 3); + break; + default: + g_set_error(err, BT_IO_ERROR, BT_IO_ERROR_INVALID_ARGS, + "Unknown option %d", opt); + return FALSE; + } + + opt = va_arg(args, int); + } + + return TRUE; +} + +static int sco_get_info(int sock, uint16_t *handle, uint8_t *dev_class) +{ + struct sco_conninfo info; + socklen_t len; + + len = sizeof(info); + if (getsockopt(sock, SOL_SCO, SCO_CONNINFO, &info, &len) < 0) + return -errno; + + if (handle) + *handle = info.hci_handle; + + if (dev_class) + memcpy(dev_class, info.dev_class, 3); + + return 0; +} + +static gboolean sco_get(int sock, GError **err, BtIOOption opt1, va_list args) +{ + BtIOOption opt = opt1; + struct sockaddr_sco src, dst; + struct sco_options sco_opt; + socklen_t len; + uint8_t dev_class[3]; + uint16_t handle; + + len = sizeof(sco_opt); + memset(&sco_opt, 0, len); + if (getsockopt(sock, SOL_SCO, SCO_OPTIONS, &sco_opt, &len) < 0) { + ERROR_FAILED(err, "getsockopt(SCO_OPTIONS)", errno); + return FALSE; + } + + if (!get_peers(sock, (struct sockaddr *) &src, + (struct sockaddr *) &dst, sizeof(src), err)) + return FALSE; + + while (opt != BT_IO_OPT_INVALID) { + switch (opt) { + case BT_IO_OPT_SOURCE: + ba2str(&src.sco_bdaddr, va_arg(args, char *)); + break; + case BT_IO_OPT_SOURCE_BDADDR: + bacpy(va_arg(args, bdaddr_t *), &src.sco_bdaddr); + break; + case BT_IO_OPT_DEST: + ba2str(&dst.sco_bdaddr, va_arg(args, char *)); + break; + case BT_IO_OPT_DEST_BDADDR: + bacpy(va_arg(args, bdaddr_t *), &dst.sco_bdaddr); + break; + case BT_IO_OPT_MTU: + case BT_IO_OPT_IMTU: + case BT_IO_OPT_OMTU: + *(va_arg(args, uint16_t *)) = sco_opt.mtu; + break; + case BT_IO_OPT_HANDLE: + if (sco_get_info(sock, &handle, dev_class) < 0) { + ERROR_FAILED(err, "SCO_CONNINFO", errno); + return FALSE; + } + *(va_arg(args, uint16_t *)) = handle; + break; + case BT_IO_OPT_CLASS: + if (sco_get_info(sock, &handle, dev_class) < 0) { + ERROR_FAILED(err, "SCO_CONNINFO", errno); + return FALSE; + } + memcpy(va_arg(args, uint8_t *), dev_class, 3); + break; + default: + g_set_error(err, BT_IO_ERROR, BT_IO_ERROR_INVALID_ARGS, + "Unknown option %d", opt); + return FALSE; + } + + opt = va_arg(args, int); + } + + return TRUE; +} + +static gboolean get_valist(GIOChannel *io, BtIOType type, GError **err, + BtIOOption opt1, va_list args) +{ + int sock; + + sock = g_io_channel_unix_get_fd(io); + + switch (type) { + case BT_IO_L2RAW: + case BT_IO_L2CAP: + case BT_IO_L2ERTM: + return l2cap_get(sock, err, opt1, args); + case BT_IO_RFCOMM: + return rfcomm_get(sock, err, opt1, args); + case BT_IO_SCO: + return sco_get(sock, err, opt1, args); + } + + g_set_error(err, BT_IO_ERROR, BT_IO_ERROR_INVALID_ARGS, + "Unknown BtIO type %d", type); + return FALSE; +} + +gboolean bt_io_accept(GIOChannel *io, BtIOConnect connect, gpointer user_data, + GDestroyNotify destroy, GError **err) +{ + int sock; + char c; + struct pollfd pfd; + + sock = g_io_channel_unix_get_fd(io); + + memset(&pfd, 0, sizeof(pfd)); + pfd.fd = sock; + pfd.events = POLLOUT; + + if (poll(&pfd, 1, 0) < 0) { + ERROR_FAILED(err, "poll", errno); + return FALSE; + } + + if (!(pfd.revents & POLLOUT)) { + if (read(sock, &c, 1) < 0) { + ERROR_FAILED(err, "read", errno); + return FALSE; + } + } + + accept_add(io, connect, user_data, destroy); + + return TRUE; +} + +gboolean bt_io_set(GIOChannel *io, BtIOType type, GError **err, + BtIOOption opt1, ...) +{ + va_list args; + gboolean ret; + struct set_opts opts; + int sock; + + va_start(args, opt1); + ret = parse_set_opts(&opts, err, opt1, args); + va_end(args); + + if (!ret) + return ret; + + sock = g_io_channel_unix_get_fd(io); + + switch (type) { + case BT_IO_L2RAW: + case BT_IO_L2CAP: + case BT_IO_L2ERTM: + return l2cap_set(sock, opts.sec_level, opts.imtu, opts.omtu, + opts.mode, opts.master, opts.flushable, + opts.priority, err); + case BT_IO_RFCOMM: + return rfcomm_set(sock, opts.sec_level, opts.master, err); + case BT_IO_SCO: + return sco_set(sock, opts.mtu, err); + } + + g_set_error(err, BT_IO_ERROR, BT_IO_ERROR_INVALID_ARGS, + "Unknown BtIO type %d", type); + return FALSE; +} + +gboolean bt_io_get(GIOChannel *io, BtIOType type, GError **err, + BtIOOption opt1, ...) +{ + va_list args; + gboolean ret; + + va_start(args, opt1); + ret = get_valist(io, type, err, opt1, args); + va_end(args); + + return ret; +} + +static GIOChannel *create_io(BtIOType type, gboolean server, + struct set_opts *opts, GError **err) +{ + int sock; + GIOChannel *io; + + switch (type) { + case BT_IO_L2RAW: + sock = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_L2CAP); + if (sock < 0) { + ERROR_FAILED(err, "socket(RAW, L2CAP)", errno); + return NULL; + } + if (l2cap_bind(sock, &opts->src, server ? opts->psm : 0, + opts->cid, err) < 0) + goto failed; + if (!l2cap_set(sock, opts->sec_level, 0, 0, 0, -1, -1, 0, err)) + goto failed; + break; + case BT_IO_L2CAP: + sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); + if (sock < 0) { + ERROR_FAILED(err, "socket(SEQPACKET, L2CAP)", errno); + return NULL; + } + if (l2cap_bind(sock, &opts->src, server ? opts->psm : 0, + opts->cid, err) < 0) + goto failed; + if (!l2cap_set(sock, opts->sec_level, opts->imtu, opts->omtu, + opts->mode, opts->master, opts->flushable, + opts->priority, err)) + goto failed; + break; + case BT_IO_L2ERTM: + sock = socket(PF_BLUETOOTH, SOCK_STREAM, BTPROTO_L2CAP); + if (sock < 0) { + ERROR_FAILED(err, "socket(STREAM, L2CAP)", errno); + return NULL; + } + if (l2cap_bind(sock, &opts->src, server ? opts->psm : 0, + opts->cid, err) < 0) + goto failed; + if (!l2cap_set(sock, opts->sec_level, opts->imtu, opts->omtu, + opts->mode, opts->master, opts->flushable, + opts->priority, err)) + goto failed; + break; + case BT_IO_RFCOMM: + sock = socket(PF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM); + if (sock < 0) { + ERROR_FAILED(err, "socket(STREAM, RFCOMM)", errno); + return NULL; + } + if (rfcomm_bind(sock, &opts->src, + server ? opts->channel : 0, err) < 0) + goto failed; + if (!rfcomm_set(sock, opts->sec_level, opts->master, err)) + goto failed; + break; + case BT_IO_SCO: + sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO); + if (sock < 0) { + ERROR_FAILED(err, "socket(SEQPACKET, SCO)", errno); + return NULL; + } + if (sco_bind(sock, &opts->src, err) < 0) + goto failed; + if (!sco_set(sock, opts->mtu, err)) + goto failed; + break; + default: + g_set_error(err, BT_IO_ERROR, BT_IO_ERROR_INVALID_ARGS, + "Unknown BtIO type %d", type); + return NULL; + } + + io = g_io_channel_unix_new(sock); + + g_io_channel_set_close_on_unref(io, TRUE); + g_io_channel_set_flags(io, G_IO_FLAG_NONBLOCK, NULL); + + return io; + +failed: + close(sock); + + return NULL; +} + +GIOChannel *bt_io_connect(BtIOType type, BtIOConnect connect, + gpointer user_data, GDestroyNotify destroy, + GError **gerr, BtIOOption opt1, ...) +{ + GIOChannel *io; + va_list args; + struct set_opts opts; + int err, sock; + gboolean ret; + + va_start(args, opt1); + ret = parse_set_opts(&opts, gerr, opt1, args); + va_end(args); + + if (ret == FALSE) + return NULL; + + io = create_io(type, FALSE, &opts, gerr); + if (io == NULL) + return NULL; + + sock = g_io_channel_unix_get_fd(io); + + switch (type) { + case BT_IO_L2RAW: + err = l2cap_connect(sock, &opts.dst, opts.dst_type, 0, + opts.cid); + break; + case BT_IO_L2CAP: + case BT_IO_L2ERTM: + err = l2cap_connect(sock, &opts.dst, opts.dst_type, + opts.psm, opts.cid); + break; + case BT_IO_RFCOMM: + err = rfcomm_connect(sock, &opts.dst, opts.channel); + break; + case BT_IO_SCO: + err = sco_connect(sock, &opts.dst); + break; + default: + g_set_error(gerr, BT_IO_ERROR, BT_IO_ERROR_INVALID_ARGS, + "Unknown BtIO type %d", type); + return NULL; + } + + if (err < 0) { + g_set_error(gerr, BT_IO_ERROR, BT_IO_ERROR_CONNECT_FAILED, + "connect: %s (%d)", strerror(-err), -err); + g_io_channel_unref(io); + return NULL; + } + + connect_add(io, connect, user_data, destroy); + + return io; +} + +GIOChannel *bt_io_listen(BtIOType type, BtIOConnect connect, + BtIOConfirm confirm, gpointer user_data, + GDestroyNotify destroy, GError **err, + BtIOOption opt1, ...) +{ + GIOChannel *io; + va_list args; + struct set_opts opts; + int sock; + gboolean ret; + + if (type == BT_IO_L2RAW) { + g_set_error(err, BT_IO_ERROR, BT_IO_ERROR_INVALID_ARGS, + "Server L2CAP RAW sockets not supported"); + return NULL; + } + + va_start(args, opt1); + ret = parse_set_opts(&opts, err, opt1, args); + va_end(args); + + if (ret == FALSE) + return NULL; + + io = create_io(type, TRUE, &opts, err); + if (io == NULL) + return NULL; + + sock = g_io_channel_unix_get_fd(io); + + if (confirm) + setsockopt(sock, SOL_BLUETOOTH, BT_DEFER_SETUP, &opts.defer, + sizeof(opts.defer)); + + if (listen(sock, 5) < 0) { + ERROR_FAILED(err, "listen", errno); + g_io_channel_unref(io); + return NULL; + } + + server_add(io, connect, confirm, user_data, destroy); + + return io; +} + +GQuark bt_io_error_quark(void) +{ + return g_quark_from_static_string("bt-io-error-quark"); +} diff --git a/btio/btio.h b/btio/btio.h new file mode 100644 index 0000000..cf0e070 --- /dev/null +++ b/btio/btio.h @@ -0,0 +1,111 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2009-2010 Marcel Holtmann + * Copyright (C) 2009-2010 Nokia Corporation + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ +#ifndef BT_IO_H +#define BT_IO_H + +#include + +typedef enum { + BT_IO_ERROR_DISCONNECTED, + BT_IO_ERROR_CONNECT_FAILED, + BT_IO_ERROR_FAILED, + BT_IO_ERROR_INVALID_ARGS, +} BtIOError; + +#define BT_IO_ERROR bt_io_error_quark() + +GQuark bt_io_error_quark(void); + +typedef enum { + BT_IO_L2RAW, + BT_IO_L2CAP, + BT_IO_L2ERTM, + BT_IO_RFCOMM, + BT_IO_SCO, +} BtIOType; + +typedef enum { + BT_IO_OPT_INVALID = 0, + BT_IO_OPT_SOURCE, + BT_IO_OPT_SOURCE_BDADDR, + BT_IO_OPT_DEST, + BT_IO_OPT_DEST_BDADDR, + BT_IO_OPT_DEST_TYPE, + BT_IO_OPT_DEFER_TIMEOUT, + BT_IO_OPT_SEC_LEVEL, + BT_IO_OPT_KEY_SIZE, + BT_IO_OPT_CHANNEL, + BT_IO_OPT_SOURCE_CHANNEL, + BT_IO_OPT_DEST_CHANNEL, + BT_IO_OPT_PSM, + BT_IO_OPT_CID, + BT_IO_OPT_MTU, + BT_IO_OPT_OMTU, + BT_IO_OPT_IMTU, + BT_IO_OPT_MASTER, + BT_IO_OPT_HANDLE, + BT_IO_OPT_CLASS, + BT_IO_OPT_MODE, + BT_IO_OPT_FLUSHABLE, + BT_IO_OPT_PRIORITY, +} BtIOOption; + +typedef enum { + BT_IO_SEC_SDP = 0, + BT_IO_SEC_LOW, + BT_IO_SEC_MEDIUM, + BT_IO_SEC_HIGH, +} BtIOSecLevel; + +typedef enum { + BT_IO_MODE_BASIC = 0, + BT_IO_MODE_RETRANS, + BT_IO_MODE_FLOWCTL, + BT_IO_MODE_ERTM, + BT_IO_MODE_STREAMING +} BtIOMode; + +typedef void (*BtIOConfirm)(GIOChannel *io, gpointer user_data); + +typedef void (*BtIOConnect)(GIOChannel *io, GError *err, gpointer user_data); + +gboolean bt_io_accept(GIOChannel *io, BtIOConnect connect, gpointer user_data, + GDestroyNotify destroy, GError **err); + +gboolean bt_io_set(GIOChannel *io, BtIOType type, GError **err, + BtIOOption opt1, ...); + +gboolean bt_io_get(GIOChannel *io, BtIOType type, GError **err, + BtIOOption opt1, ...); + +GIOChannel *bt_io_connect(BtIOType type, BtIOConnect connect, + gpointer user_data, GDestroyNotify destroy, + GError **err, BtIOOption opt1, ...); + +GIOChannel *bt_io_listen(BtIOType type, BtIOConnect connect, + BtIOConfirm confirm, gpointer user_data, + GDestroyNotify destroy, GError **err, + BtIOOption opt1, ...); + +#endif diff --git a/compat/bnep.c b/compat/bnep.c new file mode 100644 index 0000000..281350b --- /dev/null +++ b/compat/bnep.c @@ -0,0 +1,339 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2002-2003 Maxim Krasnyansky + * Copyright (C) 2002-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "pand.h" + +static int ctl; + +/* Compatibility with old ioctls */ +#define OLD_BNEPCONADD 1 +#define OLD_BNEPCONDEL 2 +#define OLD_BNEPGETCONLIST 3 +#define OLD_BNEPGETCONINFO 4 + +static unsigned long bnepconnadd; +static unsigned long bnepconndel; +static unsigned long bnepgetconnlist; +static unsigned long bnepgetconninfo; + +static struct { + char *str; + uint16_t uuid; +} __svc[] = { + { "PANU", BNEP_SVC_PANU }, + { "NAP", BNEP_SVC_NAP }, + { "GN", BNEP_SVC_GN }, + { NULL } +}; + +int bnep_str2svc(char *svc, uint16_t *uuid) +{ + int i; + for (i = 0; __svc[i].str; i++) + if (!strcasecmp(svc, __svc[i].str)) { + *uuid = __svc[i].uuid; + return 0; + } + return -1; +} + +char *bnep_svc2str(uint16_t uuid) +{ + int i; + for (i = 0; __svc[i].str; i++) + if (__svc[i].uuid == uuid) + return __svc[i].str; + return NULL; +} + +int bnep_init(void) +{ + ctl = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_BNEP); + if (ctl < 0) { + perror("Failed to open control socket"); + return 1; + } + + /* Temporary ioctl compatibility hack */ + { + struct bnep_connlist_req req; + struct bnep_conninfo ci[1]; + + req.cnum = 1; + req.ci = ci; + + if (!ioctl(ctl, BNEPGETCONNLIST, &req)) { + /* New ioctls */ + bnepconnadd = BNEPCONNADD; + bnepconndel = BNEPCONNDEL; + bnepgetconnlist = BNEPGETCONNLIST; + bnepgetconninfo = BNEPGETCONNINFO; + } else { + /* Old ioctls */ + bnepconnadd = OLD_BNEPCONADD; + bnepconndel = OLD_BNEPCONDEL; + bnepgetconnlist = OLD_BNEPGETCONLIST; + bnepgetconninfo = OLD_BNEPGETCONINFO; + } + } + + return 0; +} + +int bnep_cleanup(void) +{ + close(ctl); + return 0; +} + +int bnep_show_connections(void) +{ + struct bnep_connlist_req req; + struct bnep_conninfo ci[48]; + unsigned int i; + + req.cnum = 48; + req.ci = ci; + if (ioctl(ctl, bnepgetconnlist, &req)) { + perror("Failed to get connection list"); + return -1; + } + + for (i = 0; i < req.cnum; i++) { + char addr[18]; + ba2str((bdaddr_t *) ci[i].dst, addr); + printf("%s %s %s\n", ci[i].device, + addr, bnep_svc2str(ci[i].role)); + } + return 0; +} + +int bnep_kill_connection(uint8_t *dst) +{ + struct bnep_conndel_req req; + + memcpy(req.dst, dst, ETH_ALEN); + req.flags = 0; + if (ioctl(ctl, bnepconndel, &req)) { + perror("Failed to kill connection"); + return -1; + } + return 0; +} + +int bnep_kill_all_connections(void) +{ + struct bnep_connlist_req req; + struct bnep_conninfo ci[48]; + unsigned int i; + + req.cnum = 48; + req.ci = ci; + if (ioctl(ctl, bnepgetconnlist, &req)) { + perror("Failed to get connection list"); + return -1; + } + + for (i = 0; i < req.cnum; i++) { + struct bnep_conndel_req req; + memcpy(req.dst, ci[i].dst, ETH_ALEN); + req.flags = 0; + ioctl(ctl, bnepconndel, &req); + } + return 0; +} + +static int bnep_connadd(int sk, uint16_t role, char *dev) +{ + struct bnep_connadd_req req; + + strncpy(req.device, dev, 16); + req.device[15] = '\0'; + req.sock = sk; + req.role = role; + if (ioctl(ctl, bnepconnadd, &req)) + return -1; + strncpy(dev, req.device, 16); + return 0; +} + +struct __service_16 { + uint16_t dst; + uint16_t src; +} __attribute__ ((packed)); + +struct __service_32 { + uint16_t unused1; + uint16_t dst; + uint16_t unused2; + uint16_t src; +} __attribute__ ((packed)); + +struct __service_128 { + uint16_t unused1; + uint16_t dst; + uint16_t unused2[8]; + uint16_t src; + uint16_t unused3[7]; +} __attribute__ ((packed)); + +int bnep_accept_connection(int sk, uint16_t role, char *dev) +{ + struct bnep_setup_conn_req *req; + struct bnep_control_rsp *rsp; + unsigned char pkt[BNEP_MTU]; + ssize_t r; + + r = recv(sk, pkt, BNEP_MTU, 0); + if (r <= 0) + return -1; + + errno = EPROTO; + + if ((size_t) r < sizeof(*req)) + return -1; + + req = (void *) pkt; + + /* Highest known Control command ID + * is BNEP_FILTER_MULT_ADDR_RSP = 0x06 */ + if (req->type == BNEP_CONTROL && + req->ctrl > BNEP_FILTER_MULT_ADDR_RSP) { + uint8_t pkt[3]; + + pkt[0] = BNEP_CONTROL; + pkt[1] = BNEP_CMD_NOT_UNDERSTOOD; + pkt[2] = req->ctrl; + + send(sk, pkt, sizeof(pkt), 0); + + return -1; + } + + if (req->type != BNEP_CONTROL || req->ctrl != BNEP_SETUP_CONN_REQ) + return -1; + + /* FIXME: Check role UUIDs */ + + rsp = (void *) pkt; + rsp->type = BNEP_CONTROL; + rsp->ctrl = BNEP_SETUP_CONN_RSP; + rsp->resp = htons(BNEP_SUCCESS); + if (send(sk, rsp, sizeof(*rsp), 0) < 0) + return -1; + + return bnep_connadd(sk, role, dev); +} + +/* Create BNEP connection + * sk - Connect L2CAP socket + * role - Local role + * service - Remote service + * dev - Network device (contains actual dev name on return) + */ +int bnep_create_connection(int sk, uint16_t role, uint16_t svc, char *dev) +{ + struct bnep_setup_conn_req *req; + struct bnep_control_rsp *rsp; + struct __service_16 *s; + struct timeval timeo; + unsigned char pkt[BNEP_MTU]; + ssize_t r; + + /* Send request */ + req = (void *) pkt; + req->type = BNEP_CONTROL; + req->ctrl = BNEP_SETUP_CONN_REQ; + req->uuid_size = 2; /* 16bit UUID */ + + s = (void *) req->service; + s->dst = htons(svc); + s->src = htons(role); + + memset(&timeo, 0, sizeof(timeo)); + timeo.tv_sec = 30; + + setsockopt(sk, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo)); + + if (send(sk, pkt, sizeof(*req) + sizeof(*s), 0) < 0) + return -1; + +receive: + /* Get response */ + r = recv(sk, pkt, BNEP_MTU, 0); + if (r <= 0) + return -1; + + memset(&timeo, 0, sizeof(timeo)); + timeo.tv_sec = 0; + + setsockopt(sk, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo)); + + errno = EPROTO; + + if ((size_t) r < sizeof(*rsp)) + return -1; + + rsp = (void *) pkt; + if (rsp->type != BNEP_CONTROL) + return -1; + + if (rsp->ctrl != BNEP_SETUP_CONN_RSP) + goto receive; + + r = ntohs(rsp->resp); + + switch (r) { + case BNEP_SUCCESS: + break; + + case BNEP_CONN_INVALID_DST: + case BNEP_CONN_INVALID_SRC: + case BNEP_CONN_INVALID_SVC: + errno = EPROTO; + return -1; + + case BNEP_CONN_NOT_ALLOWED: + errno = EACCES; + return -1; + } + + return bnep_connadd(sk, role, dev); +} diff --git a/compat/dun.c b/compat/dun.c new file mode 100644 index 0000000..3f3a0d4 --- /dev/null +++ b/compat/dun.c @@ -0,0 +1,334 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2002-2003 Maxim Krasnyansky + * Copyright (C) 2002-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "dund.h" +#include "lib.h" + +#define PROC_BASE "/proc" + +static int for_each_port(int (*func)(struct rfcomm_dev_info *, unsigned long), unsigned long arg) +{ + struct rfcomm_dev_list_req *dl; + struct rfcomm_dev_info *di; + long r = 0; + int sk, i; + + sk = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_RFCOMM); + if (sk < 0 ) { + perror("Can't open RFCOMM control socket"); + exit(1); + } + + dl = malloc(sizeof(*dl) + RFCOMM_MAX_DEV * sizeof(*di)); + if (!dl) { + perror("Can't allocate request memory"); + close(sk); + exit(1); + } + + dl->dev_num = RFCOMM_MAX_DEV; + di = dl->dev_info; + + if (ioctl(sk, RFCOMMGETDEVLIST, (void *) dl) < 0) { + perror("Can't get device list"); + exit(1); + } + + for (i = 0; i < dl->dev_num; i++) { + r = func(di + i, arg); + if (r) break; + } + + close(sk); + free(dl); + return r; +} + +static int uses_rfcomm(char *path, char *dev) +{ + struct dirent *de; + DIR *dir; + + dir = opendir(path); + if (!dir) + return 0; + + if (chdir(path) < 0) + return 0; + + while ((de = readdir(dir)) != NULL) { + char link[PATH_MAX + 1]; + int len = readlink(de->d_name, link, PATH_MAX); + if (len > 0) { + link[len] = 0; + if (strstr(link, dev)) { + closedir(dir); + return 1; + } + } + } + + closedir(dir); + + return 0; +} + +static int find_pppd(int id, pid_t *pid) +{ + struct dirent *de; + char path[PATH_MAX + 1]; + char dev[10]; + int empty = 1; + DIR *dir; + + dir = opendir(PROC_BASE); + if (!dir) { + perror(PROC_BASE); + return -1; + } + + sprintf(dev, "rfcomm%d", id); + + *pid = 0; + while ((de = readdir(dir)) != NULL) { + empty = 0; + if (isdigit(de->d_name[0])) { + sprintf(path, "%s/%s/fd", PROC_BASE, de->d_name); + if (uses_rfcomm(path, dev)) { + *pid = atoi(de->d_name); + break; + } + } + } + closedir(dir); + + if (empty) + fprintf(stderr, "%s is empty (not mounted ?)\n", PROC_BASE); + + return *pid != 0; +} + +static int dun_exec(char *tty, char *prog, char **args) +{ + int pid = fork(); + int fd; + + switch (pid) { + case -1: + return -1; + + case 0: + break; + + default: + return pid; + } + + setsid(); + + /* Close all FDs */ + for (fd = 3; fd < 20; fd++) + close(fd); + + execvp(prog, args); + + syslog(LOG_ERR, "Error while executing %s", prog); + + exit(1); +} + +static int dun_create_tty(int sk, char *tty, int size) +{ + struct sockaddr_rc sa; + struct stat st; + socklen_t alen; + int id, try = 30; + + struct rfcomm_dev_req req = { + .flags = (1 << RFCOMM_REUSE_DLC) | (1 << RFCOMM_RELEASE_ONHUP), + .dev_id = -1 + }; + + alen = sizeof(sa); + if (getpeername(sk, (struct sockaddr *) &sa, &alen) < 0) + return -1; + bacpy(&req.dst, &sa.rc_bdaddr); + + alen = sizeof(sa); + if (getsockname(sk, (struct sockaddr *) &sa, &alen) < 0) + return -1; + bacpy(&req.src, &sa.rc_bdaddr); + req.channel = sa.rc_channel; + + id = ioctl(sk, RFCOMMCREATEDEV, &req); + if (id < 0) + return id; + + snprintf(tty, size, "/dev/rfcomm%d", id); + while (stat(tty, &st) < 0) { + snprintf(tty, size, "/dev/bluetooth/rfcomm/%d", id); + if (stat(tty, &st) < 0) { + snprintf(tty, size, "/dev/rfcomm%d", id); + if (try--) { + usleep(100 * 1000); + continue; + } + + memset(&req, 0, sizeof(req)); + req.dev_id = id; + ioctl(sk, RFCOMMRELEASEDEV, &req); + + return -1; + } + } + + return id; +} + +int dun_init(void) +{ + return 0; +} + +int dun_cleanup(void) +{ + return 0; +} + +static int show_conn(struct rfcomm_dev_info *di, unsigned long arg) +{ + pid_t pid; + + if (di->state == BT_CONNECTED && + (di->flags & (1<flags & (1<flags & (1<id, &pid)) { + char dst[18]; + ba2str(&di->dst, dst); + + printf("rfcomm%d: %s channel %d pppd pid %d\n", + di->id, dst, di->channel, pid); + } + } + return 0; +} + +static int kill_conn(struct rfcomm_dev_info *di, unsigned long arg) +{ + bdaddr_t *dst = (bdaddr_t *) arg; + pid_t pid; + + if (di->state == BT_CONNECTED && + (di->flags & (1<flags & (1<flags & (1<dst, dst)) + return 0; + + if (find_pppd(di->id, &pid)) { + if (kill(pid, SIGINT) < 0) + perror("Kill"); + + if (!dst) + return 0; + return 1; + } + } + return 0; +} + +int dun_show_connections(void) +{ + for_each_port(show_conn, 0); + return 0; +} + +int dun_kill_connection(uint8_t *dst) +{ + for_each_port(kill_conn, (unsigned long) dst); + return 0; +} + +int dun_kill_all_connections(void) +{ + for_each_port(kill_conn, 0); + return 0; +} + +int dun_open_connection(int sk, char *pppd, char **args, int wait) +{ + char tty[100]; + int pid; + + if (dun_create_tty(sk, tty, sizeof(tty) - 1) < 0) { + syslog(LOG_ERR, "RFCOMM TTY creation failed. %s(%d)", strerror(errno), errno); + return -1; + } + + args[0] = "pppd"; + args[1] = tty; + args[2] = "nodetach"; + + pid = dun_exec(tty, pppd, args); + if (pid < 0) { + syslog(LOG_ERR, "Exec failed. %s(%d)", strerror(errno), errno); + return -1; + } + + if (wait) { + int status; + waitpid(pid, &status, 0); + /* FIXME: Check for waitpid errors */ + } + + return 0; +} diff --git a/compat/dund.1 b/compat/dund.1 new file mode 100644 index 0000000..09fb7f7 --- /dev/null +++ b/compat/dund.1 @@ -0,0 +1,72 @@ +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.29. +.TH BlueZ "1" "February 2003" "DUN daemon" "User Commands" +.SH NAME +dund \- BlueZ Bluetooth dial-up networking daemon +.SH DESCRIPTION +DUN daemon +.SH SYNOPSIS +dund [pppd options] +.SH OPTIONS +.TP +\fB\-\-show\fR \fB\-\-list\fR \fB\-l\fR +Show active DUN connections +.TP +\fB\-\-listen\fR \fB\-s\fR +Listen for DUN connections +.TP +\fB\-\-dialup\fR \fB\-u\fR +Listen for dialup/telephone connections +.TP +\fB\-\-connect\fR \fB\-c\fR +Create DUN connection +.TP +\fB\-\-mrouter\fR \fB\-m\fR +Create mRouter connection +.TP +\fB\-\-search\fR \fB\-Q[duration]\fR +Search and connect +.TP +\fB\-\-kill\fR \fB\-k\fR +Kill DUN connection +.TP +\fB\-\-killall\fR \fB\-K\fR +Kill all DUN connections +.TP +\fB\-\-channel\fR \fB\-C\fR +RFCOMM channel +.TP +\fB\-\-device\fR \fB\-i\fR +Source bdaddr +.TP +\fB\-\-nosdp\fR \fB\-D\fR +Disable SDP +.TP +\fB\-\-auth\fR \fB\-A\fR +Enable authentification +.TP +\fB\-\-encrypt\fR \fB\-E\fR +Enable encryption +.TP +\fB\-\-secure\fR \fB\-S\fR +Secure connection +.TP +\fB\-\-master\fR \fB\-M\fR +Become the master of a piconet +.TP +\fB\-\-nodetach\fR \fB\-n\fR +Do not become a daemon +.TP +\fB\-\-persist\fR \fB\-p[interval]\fR +Persist mode +.TP +\fB\-\-pppd\fR \fB\-d\fR +Location of the PPP daemon (pppd) +.TP +\fB\-\-msdun\fR \fB\-X\fR [timeo] +Enable Microsoft dialup networking support +.TP +\fB\-\-activesync\fR \fB\-a\fR +Enable Microsoft ActiveSync networking +.TP +\fB\-\-cache\fR \fB\-C\fR [valid] +Enable address cache diff --git a/compat/dund.c b/compat/dund.c new file mode 100644 index 0000000..af1b536 --- /dev/null +++ b/compat/dund.c @@ -0,0 +1,645 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2002-2003 Maxim Krasnyansky + * Copyright (C) 2002-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include "sdp.h" +#include "dund.h" +#include "lib.h" + +volatile sig_atomic_t __io_canceled; + +/* MS dialup networking support (i.e. CLIENT / CLIENTSERVER thing) */ +static int msdun = 0; + +static char *pppd = "/usr/sbin/pppd"; +static char *pppd_opts[DUN_MAX_PPP_OPTS] = + { + /* First 3 are reserved */ + "", "", "", + "noauth", + "noipdefault", + NULL + }; + +static int detach = 1; +static int persist; +static int use_sdp = 1; +static int auth; +static int encrypt; +static int secure; +static int master; +static int type = LANACCESS; +static int search_duration = 10; +static uint use_cache; + +static int channel; + +static struct { + uint valid; + char dst[40]; + bdaddr_t bdaddr; + int channel; +} cache; + +static bdaddr_t src_addr = *BDADDR_ANY; +static int src_dev = -1; + +volatile int terminate; + +enum { + NONE, + SHOW, + LISTEN, + CONNECT, + KILL +} modes; + +static int create_connection(char *dst, bdaddr_t *bdaddr, int mrouter); + +static int do_listen(void) +{ + struct sockaddr_rc sa; + int sk, lm; + + if (type == MROUTER) { + if (!cache.valid) + return -1; + + if (create_connection(cache.dst, &cache.bdaddr, type) < 0) { + syslog(LOG_ERR, "Cannot connect to mRouter device. %s(%d)", + strerror(errno), errno); + return -1; + } + } + + if (!channel) + channel = DUN_DEFAULT_CHANNEL; + + if (use_sdp) + dun_sdp_register(&src_addr, channel, type); + + if (type == MROUTER) + syslog(LOG_INFO, "Waiting for mRouter callback on channel %d", channel); + + /* Create RFCOMM socket */ + sk = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM); + if (sk < 0) { + syslog(LOG_ERR, "Cannot create RFCOMM socket. %s(%d)", + strerror(errno), errno); + return -1; + } + + sa.rc_family = AF_BLUETOOTH; + sa.rc_channel = channel; + sa.rc_bdaddr = src_addr; + + if (bind(sk, (struct sockaddr *) &sa, sizeof(sa))) { + syslog(LOG_ERR, "Bind failed. %s(%d)", strerror(errno), errno); + return -1; + } + + /* Set link mode */ + lm = 0; + if (master) + lm |= RFCOMM_LM_MASTER; + if (auth) + lm |= RFCOMM_LM_AUTH; + if (encrypt) + lm |= RFCOMM_LM_ENCRYPT; + if (secure) + lm |= RFCOMM_LM_SECURE; + + if (lm && setsockopt(sk, SOL_RFCOMM, RFCOMM_LM, &lm, sizeof(lm)) < 0) { + syslog(LOG_ERR, "Failed to set link mode. %s(%d)", strerror(errno), errno); + return -1; + } + + listen(sk, 10); + + while (!terminate) { + socklen_t alen = sizeof(sa); + int nsk; + char ba[40]; + char ch[10]; + + nsk = accept(sk, (struct sockaddr *) &sa, &alen); + if (nsk < 0) { + syslog(LOG_ERR, "Accept failed. %s(%d)", strerror(errno), errno); + continue; + } + + switch (fork()) { + case 0: + break; + case -1: + syslog(LOG_ERR, "Fork failed. %s(%d)", strerror(errno), errno); + default: + close(nsk); + if (type == MROUTER) { + close(sk); + terminate = 1; + } + continue; + } + + close(sk); + + if (msdun && ms_dun(nsk, 1, msdun) < 0) { + syslog(LOG_ERR, "MSDUN failed. %s(%d)", strerror(errno), errno); + exit(0); + } + + ba2str(&sa.rc_bdaddr, ba); + snprintf(ch, sizeof(ch), "%d", channel); + + /* Setup environment */ + setenv("DUN_BDADDR", ba, 1); + setenv("DUN_CHANNEL", ch, 1); + + if (!dun_open_connection(nsk, pppd, pppd_opts, 0)) + syslog(LOG_INFO, "New connection from %s", ba); + + close(nsk); + exit(0); + } + + if (use_sdp) + dun_sdp_unregister(); + return 0; +} + +/* Connect and initiate RFCOMM session + * Returns: + * -1 - critical error (exit persist mode) + * 1 - non critical error + * 0 - success + */ +static int create_connection(char *dst, bdaddr_t *bdaddr, int mrouter) +{ + struct sockaddr_rc sa; + int sk, err = 0, ch; + + if (use_cache && cache.valid && cache.channel) { + /* Use cached channel */ + ch = cache.channel; + + } else if (!channel) { + syslog(LOG_INFO, "Searching for %s on %s", mrouter ? "SP" : "LAP", dst); + + if (dun_sdp_search(&src_addr, bdaddr, &ch, mrouter) <= 0) + return 0; + } else + ch = channel; + + syslog(LOG_INFO, "Connecting to %s channel %d", dst, ch); + + sk = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM); + if (sk < 0) { + syslog(LOG_ERR, "Cannot create RFCOMM socket. %s(%d)", + strerror(errno), errno); + return -1; + } + + sa.rc_family = AF_BLUETOOTH; + sa.rc_channel = 0; + sa.rc_bdaddr = src_addr; + + if (bind(sk, (struct sockaddr *) &sa, sizeof(sa))) + syslog(LOG_ERR, "Bind failed. %s(%d)", + strerror(errno), errno); + + sa.rc_channel = ch; + sa.rc_bdaddr = *bdaddr; + + if (!connect(sk, (struct sockaddr *) &sa, sizeof(sa)) ) { + if (mrouter) { + sleep(1); + close(sk); + return 0; + } + + syslog(LOG_INFO, "Connection established"); + + if (msdun && ms_dun(sk, 0, msdun) < 0) { + syslog(LOG_ERR, "MSDUN failed. %s(%d)", strerror(errno), errno); + err = 1; + goto out; + } + + if (!dun_open_connection(sk, pppd, pppd_opts, (persist > 0))) + err = 0; + else + err = 1; + } else { + syslog(LOG_ERR, "Connect to %s failed. %s(%d)", + dst, strerror(errno), errno); + err = 1; + } + +out: + if (use_cache) { + if (!err) { + /* Succesesful connection, validate cache */ + strcpy(cache.dst, dst); + bacpy(&cache.bdaddr, bdaddr); + cache.channel = ch; + cache.valid = use_cache; + } else { + cache.channel = 0; + cache.valid--; + } + } + + close(sk); + return err; +} + +/* Search and connect + * Returns: + * -1 - critical error (exit persist mode) + * 1 - non critical error + * 0 - success + */ +static int do_connect(void) +{ + inquiry_info *ii; + int reconnect = 0; + int i, n, r = 0; + + do { + if (reconnect) + sleep(persist); + reconnect = 1; + + if (cache.valid) { + /* Use cached bdaddr */ + r = create_connection(cache.dst, &cache.bdaddr, 0); + if (r < 0) { + terminate = 1; + break; + } + continue; + } + + syslog(LOG_INFO, "Inquiring"); + + /* FIXME: Should we use non general LAP here ? */ + + ii = NULL; + n = hci_inquiry(src_dev, search_duration, 0, NULL, &ii, 0); + if (n < 0) { + syslog(LOG_ERR, "Inquiry failed. %s(%d)", strerror(errno), errno); + continue; + } + + for (i = 0; i < n; i++) { + char dst[40]; + ba2str(&ii[i].bdaddr, dst); + + r = create_connection(dst, &ii[i].bdaddr, 0); + if (r < 0) { + terminate = 1; + break; + } + } + bt_free(ii); + } while (!terminate && persist); + + return r; +} + +static void do_show(void) +{ + dun_show_connections(); +} + +static void do_kill(char *dst) +{ + if (dst) { + bdaddr_t ba; + str2ba(dst, &ba); + dun_kill_connection((void *) &ba); + } else + dun_kill_all_connections(); +} + +static void sig_hup(int sig) +{ + return; +} + +static void sig_term(int sig) +{ + io_cancel(); + terminate = 1; +} + +static struct option main_lopts[] = { + { "help", 0, 0, 'h' }, + { "listen", 0, 0, 's' }, + { "connect", 1, 0, 'c' }, + { "search", 2, 0, 'Q' }, + { "kill", 1, 0, 'k' }, + { "killall", 0, 0, 'K' }, + { "channel", 1, 0, 'P' }, + { "device", 1, 0, 'i' }, + { "nosdp", 0, 0, 'D' }, + { "list", 0, 0, 'l' }, + { "show", 0, 0, 'l' }, + { "nodetach", 0, 0, 'n' }, + { "persist", 2, 0, 'p' }, + { "auth", 0, 0, 'A' }, + { "encrypt", 0, 0, 'E' }, + { "secure", 0, 0, 'S' }, + { "master", 0, 0, 'M' }, + { "cache", 0, 0, 'C' }, + { "pppd", 1, 0, 'd' }, + { "msdun", 2, 0, 'X' }, + { "activesync", 0, 0, 'a' }, + { "mrouter", 1, 0, 'm' }, + { "dialup", 0, 0, 'u' }, + { 0, 0, 0, 0 } +}; + +static const char *main_sopts = "hsc:k:Kr:i:lnp::DQ::AESMP:C::P:Xam:u"; + +static const char *main_help = + "Bluetooth LAP (LAN Access over PPP) daemon version %s\n" + "Usage:\n" + "\tdund [pppd options]\n" + "Options:\n" + "\t--show --list -l Show active LAP connections\n" + "\t--listen -s Listen for LAP connections\n" + "\t--dialup -u Pretend to be a dialup/telephone\n" + "\t--connect -c Create LAP connection\n" + "\t--mrouter -m Create mRouter connection\n" + "\t--search -Q[duration] Search and connect\n" + "\t--kill -k Kill LAP connection\n" + "\t--killall -K Kill all LAP connections\n" + "\t--channel -P RFCOMM channel\n" + "\t--device -i Source bdaddr\n" + "\t--nosdp -D Disable SDP\n" + "\t--auth -A Enable authentication\n" + "\t--encrypt -E Enable encryption\n" + "\t--secure -S Secure connection\n" + "\t--master -M Become the master of a piconet\n" + "\t--nodetach -n Do not become a daemon\n" + "\t--persist -p[interval] Persist mode\n" + "\t--pppd -d Location of the PPP daemon (pppd)\n" + "\t--msdun -X[timeo] Enable Microsoft dialup networking support\n" + "\t--activesync -a Enable Microsoft ActiveSync networking\n" + "\t--cache -C[valid] Enable address cache\n"; + +int main(int argc, char *argv[]) +{ + char *dst = NULL, *src = NULL; + struct sigaction sa; + int mode = NONE; + int opt; + + while ((opt=getopt_long(argc, argv, main_sopts, main_lopts, NULL)) != -1) { + switch(opt) { + case 'l': + mode = SHOW; + detach = 0; + break; + + case 's': + mode = LISTEN; + type = LANACCESS; + break; + + case 'c': + mode = CONNECT; + dst = strdup(optarg); + break; + + case 'Q': + mode = CONNECT; + dst = NULL; + if (optarg) + search_duration = atoi(optarg); + break; + + case 'k': + mode = KILL; + detach = 0; + dst = strdup(optarg); + break; + + case 'K': + mode = KILL; + detach = 0; + dst = NULL; + break; + + case 'P': + channel = atoi(optarg); + break; + + case 'i': + src = strdup(optarg); + break; + + case 'D': + use_sdp = 0; + break; + + case 'A': + auth = 1; + break; + + case 'E': + encrypt = 1; + break; + + case 'S': + secure = 1; + break; + + case 'M': + master = 1; + break; + + case 'n': + detach = 0; + break; + + case 'p': + if (optarg) + persist = atoi(optarg); + else + persist = 5; + break; + + case 'C': + if (optarg) + use_cache = atoi(optarg); + else + use_cache = 2; + break; + + case 'd': + pppd = strdup(optarg); + break; + + case 'X': + if (optarg) + msdun = atoi(optarg); + else + msdun = 10; + break; + + case 'a': + msdun = 10; + type = ACTIVESYNC; + break; + + case 'm': + mode = LISTEN; + dst = strdup(optarg); + type = MROUTER; + break; + + case 'u': + mode = LISTEN; + type = DIALUP; + break; + + case 'h': + default: + printf(main_help, VERSION); + exit(0); + } + } + + argc -= optind; + argv += optind; + + /* The rest is pppd options */ + if (argc > 0) { + for (opt = 3; argc && opt < DUN_MAX_PPP_OPTS - 1; + argc--, opt++) + pppd_opts[opt] = *argv++; + pppd_opts[opt] = NULL; + } + + io_init(); + + if (dun_init()) { + free(dst); + return -1; + } + + /* Check non daemon modes first */ + switch (mode) { + case SHOW: + do_show(); + free(dst); + return 0; + + case KILL: + do_kill(dst); + free(dst); + return 0; + + case NONE: + printf(main_help, VERSION); + free(dst); + return 0; + } + + /* Initialize signals */ + memset(&sa, 0, sizeof(sa)); + sa.sa_flags = SA_NOCLDSTOP; + sa.sa_handler = SIG_IGN; + sigaction(SIGCHLD, &sa, NULL); + sigaction(SIGPIPE, &sa, NULL); + + sa.sa_handler = sig_term; + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + + sa.sa_handler = sig_hup; + sigaction(SIGHUP, &sa, NULL); + + if (detach && daemon(0, 0)) { + perror("Can't start daemon"); + exit(1); + } + + openlog("dund", LOG_PID | LOG_NDELAY | LOG_PERROR, LOG_DAEMON); + syslog(LOG_INFO, "Bluetooth DUN daemon version %s", VERSION); + + if (src) { + src_dev = hci_devid(src); + if (src_dev < 0 || hci_devba(src_dev, &src_addr) < 0) { + syslog(LOG_ERR, "Invalid source. %s(%d)", strerror(errno), errno); + free(dst); + return -1; + } + } + + if (dst) { + strncpy(cache.dst, dst, sizeof(cache.dst) - 1); + str2ba(dst, &cache.bdaddr); + + /* Disable cache invalidation */ + use_cache = cache.valid = ~0; + } + + switch (mode) { + case CONNECT: + do_connect(); + break; + + case LISTEN: + do_listen(); + break; + } + + free(dst); + return 0; +} diff --git a/compat/dund.h b/compat/dund.h new file mode 100644 index 0000000..e3a4ef6 --- /dev/null +++ b/compat/dund.h @@ -0,0 +1,40 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2002-2003 Maxim Krasnyansky + * Copyright (C) 2002-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define DUN_CONFIG_DIR "/etc/bluetooth/dun" + +#define DUN_DEFAULT_CHANNEL 1 + +#define DUN_MAX_PPP_OPTS 40 + +int dun_init(void); +int dun_cleanup(void); + +int dun_show_connections(void); +int dun_kill_connection(uint8_t *dst); +int dun_kill_all_connections(void); + +int dun_open_connection(int sk, char *pppd, char **pppd_opts, int wait); + +int ms_dun(int fd, int server, int timeo); diff --git a/compat/fakehid.c b/compat/fakehid.c new file mode 100644 index 0000000..66161b3 --- /dev/null +++ b/compat/fakehid.c @@ -0,0 +1,669 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2003-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "hidd.h" +#include "uinput.h" + +#include + +#ifdef NEED_PPOLL +#include "ppoll.h" +#endif + +static volatile sig_atomic_t __io_canceled = 0; + +static void sig_hup(int sig) +{ +} + +static void sig_term(int sig) +{ + __io_canceled = 1; +} + +static int send_event(int fd, uint16_t type, uint16_t code, int32_t value) +{ + struct uinput_event event; + + if (fd <= fileno(stderr)) + return -EINVAL; + + memset(&event, 0, sizeof(event)); + event.type = type; + event.code = code; + event.value = value; + + return write(fd, &event, sizeof(event)); +} + +static int uinput_create(char *name, int keyboard, int mouse) +{ + struct uinput_dev dev; + int fd, aux; + + fd = open("/dev/uinput", O_RDWR); + if (fd < 0) { + fd = open("/dev/input/uinput", O_RDWR); + if (fd < 0) { + fd = open("/dev/misc/uinput", O_RDWR); + if (fd < 0) { + fprintf(stderr, "Can't open input device: %s (%d)\n", + strerror(errno), errno); + return -1; + } + } + } + + memset(&dev, 0, sizeof(dev)); + + if (name) + strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE - 1); + + dev.id.bustype = BUS_BLUETOOTH; + dev.id.vendor = 0x0000; + dev.id.product = 0x0000; + dev.id.version = 0x0000; + + if (write(fd, &dev, sizeof(dev)) < 0) { + fprintf(stderr, "Can't write device information: %s (%d)\n", + strerror(errno), errno); + close(fd); + return -1; + } + + if (mouse) { + ioctl(fd, UI_SET_EVBIT, EV_REL); + + for (aux = REL_X; aux <= REL_MISC; aux++) + ioctl(fd, UI_SET_RELBIT, aux); + } + + if (keyboard) { + ioctl(fd, UI_SET_EVBIT, EV_KEY); + ioctl(fd, UI_SET_EVBIT, EV_LED); + ioctl(fd, UI_SET_EVBIT, EV_REP); + + for (aux = KEY_RESERVED; aux <= KEY_UNKNOWN; aux++) + ioctl(fd, UI_SET_KEYBIT, aux); + /* + *for (aux = LED_NUML; aux <= LED_MISC; aux++) + * ioctl(fd, UI_SET_LEDBIT, aux); + */ + } + + if (mouse) { + ioctl(fd, UI_SET_EVBIT, EV_KEY); + + for (aux = BTN_LEFT; aux <= BTN_BACK; aux++) + ioctl(fd, UI_SET_KEYBIT, aux); + } + + ioctl(fd, UI_DEV_CREATE); + + return fd; +} + +static int rfcomm_connect(const bdaddr_t *src, const bdaddr_t *dst, uint8_t channel) +{ + struct sockaddr_rc addr; + int sk; + + sk = socket(PF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM); + if (sk < 0) { + fprintf(stderr, "Can't create socket: %s (%d)\n", + strerror(errno), errno); + return -1; + } + + memset(&addr, 0, sizeof(addr)); + addr.rc_family = AF_BLUETOOTH; + bacpy(&addr.rc_bdaddr, src); + + if (bind(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + fprintf(stderr, "Can't bind socket: %s (%d)\n", + strerror(errno), errno); + close(sk); + return -1; + } + + memset(&addr, 0, sizeof(addr)); + addr.rc_family = AF_BLUETOOTH; + bacpy(&addr.rc_bdaddr, dst); + addr.rc_channel = channel; + + if (connect(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + fprintf(stderr, "Can't connect: %s (%d)\n", + strerror(errno), errno); + close(sk); + return -1; + } + + return sk; +} + +static void func(int fd) +{ +} + +static void back(int fd) +{ +} + +static void next(int fd) +{ +} + +static void button(int fd, unsigned int button, int is_press) +{ + switch (button) { + case 1: + send_event(fd, EV_KEY, BTN_LEFT, is_press); + break; + case 3: + send_event(fd, EV_KEY, BTN_RIGHT, is_press); + break; + } + + send_event(fd, EV_SYN, SYN_REPORT, 0); +} + +static void move(int fd, unsigned int direction) +{ + double angle; + int32_t x, y; + + angle = (direction * 22.5) * 3.1415926 / 180; + x = (int) (sin(angle) * 8); + y = (int) (cos(angle) * -8); + + send_event(fd, EV_REL, REL_X, x); + send_event(fd, EV_REL, REL_Y, y); + + send_event(fd, EV_SYN, SYN_REPORT, 0); +} + +static inline void epox_decode(int fd, unsigned char event) +{ + switch (event) { + case 48: + func(fd); break; + case 55: + back(fd); break; + case 56: + next(fd); break; + case 53: + button(fd, 1, 1); break; + case 121: + button(fd, 1, 0); break; + case 113: + break; + case 54: + button(fd, 3, 1); break; + case 120: + button(fd, 3, 0); break; + case 112: + break; + case 51: + move(fd, 0); break; + case 97: + move(fd, 1); break; + case 65: + move(fd, 2); break; + case 98: + move(fd, 3); break; + case 50: + move(fd, 4); break; + case 99: + move(fd, 5); break; + case 67: + move(fd, 6); break; + case 101: + move(fd, 7); break; + case 52: + move(fd, 8); break; + case 100: + move(fd, 9); break; + case 66: + move(fd, 10); break; + case 102: + move(fd, 11); break; + case 49: + move(fd, 12); break; + case 103: + move(fd, 13); break; + case 57: + move(fd, 14); break; + case 104: + move(fd, 15); break; + case 69: + break; + default: + printf("Unknown event code %d\n", event); + break; + } +} + +int epox_presenter(const bdaddr_t *src, const bdaddr_t *dst, uint8_t channel) +{ + unsigned char buf[16]; + struct sigaction sa; + struct pollfd p; + sigset_t sigs; + char addr[18]; + int i, fd, sk, len; + + sk = rfcomm_connect(src, dst, channel); + if (sk < 0) + return -1; + + fd = uinput_create("Bluetooth Presenter", 0, 1); + if (fd < 0) { + close(sk); + return -1; + } + + ba2str(dst, addr); + + printf("Connected to %s on channel %d\n", addr, channel); + printf("Press CTRL-C for hangup\n"); + + memset(&sa, 0, sizeof(sa)); + sa.sa_flags = SA_NOCLDSTOP; + sa.sa_handler = SIG_IGN; + sigaction(SIGCHLD, &sa, NULL); + sigaction(SIGPIPE, &sa, NULL); + + sa.sa_handler = sig_term; + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + + sa.sa_handler = sig_hup; + sigaction(SIGHUP, &sa, NULL); + + sigfillset(&sigs); + sigdelset(&sigs, SIGCHLD); + sigdelset(&sigs, SIGPIPE); + sigdelset(&sigs, SIGTERM); + sigdelset(&sigs, SIGINT); + sigdelset(&sigs, SIGHUP); + + p.fd = sk; + p.events = POLLIN | POLLERR | POLLHUP; + + while (!__io_canceled) { + p.revents = 0; + if (ppoll(&p, 1, NULL, &sigs) < 1) + continue; + + len = read(sk, buf, sizeof(buf)); + if (len < 0) + break; + + for (i = 0; i < len; i++) + epox_decode(fd, buf[i]); + } + + printf("Disconnected\n"); + + ioctl(fd, UI_DEV_DESTROY); + + close(fd); + close(sk); + + return 0; +} + +int headset_presenter(const bdaddr_t *src, const bdaddr_t *dst, uint8_t channel) +{ + printf("Not implemented\n"); + return -1; +} + +/* The strange meta key close to Ctrl has been assigned to Esc, + Fn key to CtrlR and the left space to Alt*/ + +static unsigned char jthree_keycodes[63] = { + KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, + KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, + KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, + KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, + KEY_LEFTALT, KEY_TAB, KEY_CAPSLOCK, KEY_ESC, + KEY_7, KEY_8, KEY_9, KEY_0, KEY_MINUS, KEY_EQUAL, KEY_BACKSPACE, + KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P, KEY_LEFTBRACE, KEY_RIGHTBRACE, + KEY_H, KEY_J, KEY_K, KEY_L, KEY_SEMICOLON, KEY_APOSTROPHE, KEY_ENTER, + KEY_N, KEY_M, KEY_COMMA, KEY_DOT, KEY_SLASH, KEY_UP, + KEY_SPACE, KEY_COMPOSE, KEY_LEFT, KEY_DOWN, KEY_RIGHT, + KEY_LEFTCTRL, KEY_RIGHTSHIFT, KEY_LEFTSHIFT, KEY_DELETE, KEY_RIGHTCTRL, KEY_RIGHTALT, +}; + +static inline void jthree_decode(int fd, unsigned char event) +{ + if (event > 63) + send_event(fd, EV_KEY, jthree_keycodes[event & 0x3f], 0); + else + send_event(fd, EV_KEY, jthree_keycodes[event - 1], 1); +} + +int jthree_keyboard(const bdaddr_t *src, const bdaddr_t *dst, uint8_t channel) +{ + unsigned char buf[16]; + struct sigaction sa; + struct pollfd p; + sigset_t sigs; + char addr[18]; + int i, fd, sk, len; + + sk = rfcomm_connect(src, dst, channel); + if (sk < 0) + return -1; + + fd = uinput_create("J-Three Keyboard", 1, 0); + if (fd < 0) { + close(sk); + return -1; + } + + ba2str(dst, addr); + + printf("Connected to %s on channel %d\n", addr, channel); + printf("Press CTRL-C for hangup\n"); + + memset(&sa, 0, sizeof(sa)); + sa.sa_flags = SA_NOCLDSTOP; + sa.sa_handler = SIG_IGN; + sigaction(SIGCHLD, &sa, NULL); + sigaction(SIGPIPE, &sa, NULL); + + sa.sa_handler = sig_term; + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + + sa.sa_handler = sig_hup; + sigaction(SIGHUP, &sa, NULL); + + sigfillset(&sigs); + sigdelset(&sigs, SIGCHLD); + sigdelset(&sigs, SIGPIPE); + sigdelset(&sigs, SIGTERM); + sigdelset(&sigs, SIGINT); + sigdelset(&sigs, SIGHUP); + + p.fd = sk; + p.events = POLLIN | POLLERR | POLLHUP; + + while (!__io_canceled) { + p.revents = 0; + if (ppoll(&p, 1, NULL, &sigs) < 1) + continue; + + len = read(sk, buf, sizeof(buf)); + if (len < 0) + break; + + for (i = 0; i < len; i++) + jthree_decode(fd, buf[i]); + } + + printf("Disconnected\n"); + + ioctl(fd, UI_DEV_DESTROY); + + close(fd); + close(sk); + + return 0; +} + +static const int celluon_xlate_num[10] = { + KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9 +}; + +static const int celluon_xlate_char[26] = { + KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J, + KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, + KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z +}; + +static int celluon_xlate(int c) +{ + if (c >= '0' && c <= '9') + return celluon_xlate_num[c - '0']; + + if (c >= 'A' && c <= 'Z') + return celluon_xlate_char[c - 'A']; + + switch (c) { + case 0x08: + return KEY_BACKSPACE; + case 0x09: + return KEY_TAB; + case 0x0d: + return KEY_ENTER; + case 0x11: + return KEY_LEFTCTRL; + case 0x14: + return KEY_CAPSLOCK; + case 0x20: + return KEY_SPACE; + case 0x25: + return KEY_LEFT; + case 0x26: + return KEY_UP; + case 0x27: + return KEY_RIGHT; + case 0x28: + return KEY_DOWN; + case 0x2e: + return KEY_DELETE; + case 0x5b: + return KEY_MENU; + case 0xa1: + return KEY_RIGHTSHIFT; + case 0xa0: + return KEY_LEFTSHIFT; + case 0xba: + return KEY_SEMICOLON; + case 0xbd: + return KEY_MINUS; + case 0xbc: + return KEY_COMMA; + case 0xbb: + return KEY_EQUAL; + case 0xbe: + return KEY_DOT; + case 0xbf: + return KEY_SLASH; + case 0xc0: + return KEY_GRAVE; + case 0xdb: + return KEY_LEFTBRACE; + case 0xdc: + return KEY_BACKSLASH; + case 0xdd: + return KEY_RIGHTBRACE; + case 0xde: + return KEY_APOSTROPHE; + case 0xff03: + return KEY_HOMEPAGE; + case 0xff04: + return KEY_TIME; + case 0xff06: + return KEY_OPEN; + case 0xff07: + return KEY_LIST; + case 0xff08: + return KEY_MAIL; + case 0xff30: + return KEY_CALC; + case 0xff1a: /* Map FN to ALT */ + return KEY_LEFTALT; + case 0xff2f: + return KEY_INFO; + default: + printf("Unknown key %x\n", c); + return c; + } +} + +struct celluon_state { + int len; /* Expected length of current packet */ + int count; /* Number of bytes received */ + int action; + int key; +}; + +static void celluon_decode(int fd, struct celluon_state *s, uint8_t c) +{ + if (s->count < 2 && c != 0xa5) { + /* Lost Sync */ + s->count = 0; + return; + } + + switch (s->count) { + case 0: + /* New packet - Reset state */ + s->len = 30; + s->key = 0; + break; + case 1: + break; + case 6: + s->action = c; + break; + case 28: + s->key = c; + if (c == 0xff) + s->len = 31; + break; + case 29: + case 30: + if (s->count == s->len - 1) { + /* TODO: Verify checksum */ + if (s->action < 2) { + send_event(fd, EV_KEY, celluon_xlate(s->key), + s->action); + } + s->count = -1; + } else { + s->key = (s->key << 8) | c; + } + break; + } + + s->count++; + + return; +} + +int celluon_keyboard(const bdaddr_t *src, const bdaddr_t *dst, uint8_t channel) +{ + unsigned char buf[16]; + struct sigaction sa; + struct pollfd p; + sigset_t sigs; + char addr[18]; + int i, fd, sk, len; + struct celluon_state s; + + sk = rfcomm_connect(src, dst, channel); + if (sk < 0) + return -1; + + fd = uinput_create("Celluon Keyboard", 1, 0); + if (fd < 0) { + close(sk); + return -1; + } + + ba2str(dst, addr); + + printf("Connected to %s on channel %d\n", addr, channel); + printf("Press CTRL-C for hangup\n"); + + memset(&sa, 0, sizeof(sa)); + sa.sa_flags = SA_NOCLDSTOP; + sa.sa_handler = SIG_IGN; + sigaction(SIGCHLD, &sa, NULL); + sigaction(SIGPIPE, &sa, NULL); + + sa.sa_handler = sig_term; + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + + sa.sa_handler = sig_hup; + sigaction(SIGHUP, &sa, NULL); + + sigfillset(&sigs); + sigdelset(&sigs, SIGCHLD); + sigdelset(&sigs, SIGPIPE); + sigdelset(&sigs, SIGTERM); + sigdelset(&sigs, SIGINT); + sigdelset(&sigs, SIGHUP); + + p.fd = sk; + p.events = POLLIN | POLLERR | POLLHUP; + + memset(&s, 0, sizeof(s)); + + while (!__io_canceled) { + p.revents = 0; + if (ppoll(&p, 1, NULL, &sigs) < 1) + continue; + + len = read(sk, buf, sizeof(buf)); + if (len < 0) + break; + + for (i = 0; i < len; i++) + celluon_decode(fd, &s, buf[i]); + } + + printf("Disconnected\n"); + + ioctl(fd, UI_DEV_DESTROY); + + close(fd); + close(sk); + + return 0; +} diff --git a/compat/fakehid.txt b/compat/fakehid.txt new file mode 100644 index 0000000..000d0ee --- /dev/null +++ b/compat/fakehid.txt @@ -0,0 +1,134 @@ +EPox Presenter +============== + +# hcitool inq +Inquiring ... + 00:04:61:aa:bb:cc clock offset: 0x1ded class: 0x004000 + +# hcitool info 00:04:61:aa:bb:cc +Requesting information ... + BD Address: 00:04:61:aa:bb:cc + OUI Company: EPOX Computer Co., Ltd. (00-04-61) + Device Name: EPox BT-PM01B aabbcc + LMP Version: 1.1 (0x1) LMP Subversion: 0xf78 + Manufacturer: Cambridge Silicon Radio (10) + Features: 0xff 0xff 0x0f 0x00 0x00 0x00 0x00 0x00 + <3-slot packets> <5-slot packets> + + + + + +# sdptool records --raw 00:04:61:aa:bb:cc +Sequence + Attribute 0x0000 - ServiceRecordHandle + UINT32 0x00010000 + Attribute 0x0001 - ServiceClassIDList + Sequence + UUID16 0x1101 - SerialPort + Attribute 0x0004 - ProtocolDescriptorList + Sequence + Sequence + UUID16 0x0100 - L2CAP + Sequence + UUID16 0x0003 - RFCOMM + UINT8 0x01 + Attribute 0x0100 + String Cable Replacement + + +J-Three Keyboard +================ + +# hcitool inq +Inquiring ... + 00:0A:3A:aa:bb:cc clock offset: 0x3039 class: 0x001f00 + +# hcitool info 00:0A:3A:aa:bb:cc +Password: +Requesting information ... + BD Address: 00:0A:3A:aa:bb:cc + OUI Company: J-THREE INTERNATIONAL Holding Co., Ltd. (00-0A-3A) + Device Name: KEYBOARD + LMP Version: 1.1 (0x1) LMP Subversion: 0x2c2 + Manufacturer: Cambridge Silicon Radio (10) + Features: 0xbc 0x06 0x07 0x00 0x00 0x00 0x00 0x00 + + + + +# sdptool records --raw 00:0A:3A:aa:bb:cc +Sequence + Attribute 0x0000 - ServiceRecordHandle + UINT32 0x00010000 + Attribute 0x0001 - ServiceClassIDList + Sequence + UUID16 0x1101 - SerialPort + Attribute 0x0004 - ProtocolDescriptorList + Sequence + Sequence + UUID16 0x0100 - L2CAP + Sequence + UUID16 0x0003 - RFCOMM + UINT8 0x01 + Attribute 0x0006 - LanguageBaseAttributeIDList + Sequence + UINT16 0x656e + UINT16 0x006a + UINT16 0x0100 + Attribute 0x0100 + String SPP slave + + +Celluon Laserkey Keyboard +========================= + +# hcitool inq +Inquiring ... + 00:0B:24:aa:bb:cc clock offset: 0x3ab6 class: 0x400210 + +# hcitool info 00:0B:24:aa:bb:cc +Requesting information ... + BD Address: 00:0B:24:aa:bb:cc + OUI Company: AirLogic (00-0B-24) + Device Name: CL800BT + LMP Version: 1.1 (0x1) LMP Subversion: 0x291 + Manufacturer: Cambridge Silicon Radio (10) + Features: 0xff 0xff 0x0f 0x00 0x00 0x00 0x00 0x00 + <3-slot packets> <5-slot packets> + + + + + +# sdptool records --raw 00:0B:24:aa:bb:cc +Sequence + Attribute 0x0000 - ServiceRecordHandle + UINT32 0x00010000 + Attribute 0x0001 - ServiceClassIDList + Sequence + UUID16 0x1101 - SerialPort + Attribute 0x0004 - ProtocolDescriptorList + Sequence + Sequence + UUID16 0x0100 - L2CAP + Sequence + UUID16 0x0003 - RFCOMM + UINT8 0x01 + Attribute 0x0100 + String Serial Port + +Packet format is as follows (all fields little-endian): + 0 uint16 magic # 0x5a5a + 2 uint32 unknown # ??? + 6 uint8 action # 0 = keyup, 1 = keydown, 2 = repeat + # 3, 4, 5, 6 = ??? (Mouse mode) + 7 uint8 unknown[9] # ??? + 16 uint8 action2 # ??? same as action + 17 uint16 x # Horizontal coordinate + 19 uint16 y # Vertical coordinate + 21 uint16 time # Some sort of timestamp + 23 uint8 unknown[5] # ??? + 28 uint8 key[] # single byte keycode or 0xff byte + # follwed by special keycode byte. + Each packet followed by a checksum byte. diff --git a/compat/hidd.1 b/compat/hidd.1 new file mode 100644 index 0000000..b186ac2 --- /dev/null +++ b/compat/hidd.1 @@ -0,0 +1,41 @@ +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.33. +.TH HIDD "1" "May 2004" "hidd - Bluetooth HID daemon" "User Commands" +.SH NAME +hidd \- Bluetooth HID daemon +.SH DESCRIPTION +hidd - Bluetooth HID daemon +.SS "Usage:" +.IP +hidd [options] [commands] +.SH OPTIONS +.TP +\fB\-i\fR +Local HCI device or BD Address +.TP +\fB\-t\fR +Set idle timeout (in minutes) +.TP +\fB\-n\fR, \fB\-\-nodaemon\fR +Don't fork daemon to background +.TP +\fB\-h\fR, \fB\-\-help\fR +Display help +.SS "Commands:" +.TP +\fB\-\-server\fR +Start HID server +.TP +\fB\-\-search\fR +Search for HID devices +.TP +\fB\-\-connect\fR +Connect remote HID device +.TP +\fB\-\-kill\fR +Terminate HID connection +.TP +\fB\-\-killall\fR +Terminate all connections +.TP +\fB\-\-show\fR +List current HID connections diff --git a/compat/hidd.c b/compat/hidd.c new file mode 100644 index 0000000..f8a0dfd --- /dev/null +++ b/compat/hidd.c @@ -0,0 +1,848 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2003-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "sdp.h" +#include "hidd.h" + +#ifdef NEED_PPOLL +#include "ppoll.h" +#endif + +enum { + NONE, + SHOW, + SERVER, + SEARCH, + CONNECT, + KILL +}; + +static volatile sig_atomic_t __io_canceled = 0; + +static void sig_hup(int sig) +{ +} + +static void sig_term(int sig) +{ + __io_canceled = 1; +} + +static int l2cap_connect(bdaddr_t *src, bdaddr_t *dst, unsigned short psm) +{ + struct sockaddr_l2 addr; + struct l2cap_options opts; + int sk; + + if ((sk = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP)) < 0) + return -1; + + memset(&addr, 0, sizeof(addr)); + addr.l2_family = AF_BLUETOOTH; + bacpy(&addr.l2_bdaddr, src); + + if (bind(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + close(sk); + return -1; + } + + memset(&opts, 0, sizeof(opts)); + opts.imtu = HIDP_DEFAULT_MTU; + opts.omtu = HIDP_DEFAULT_MTU; + opts.flush_to = 0xffff; + + setsockopt(sk, SOL_L2CAP, L2CAP_OPTIONS, &opts, sizeof(opts)); + + memset(&addr, 0, sizeof(addr)); + addr.l2_family = AF_BLUETOOTH; + bacpy(&addr.l2_bdaddr, dst); + addr.l2_psm = htobs(psm); + + if (connect(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + close(sk); + return -1; + } + + return sk; +} + +static int l2cap_listen(const bdaddr_t *bdaddr, unsigned short psm, int lm, int backlog) +{ + struct sockaddr_l2 addr; + struct l2cap_options opts; + int sk; + + if ((sk = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP)) < 0) + return -1; + + memset(&addr, 0, sizeof(addr)); + addr.l2_family = AF_BLUETOOTH; + bacpy(&addr.l2_bdaddr, bdaddr); + addr.l2_psm = htobs(psm); + + if (bind(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + close(sk); + return -1; + } + + setsockopt(sk, SOL_L2CAP, L2CAP_LM, &lm, sizeof(lm)); + + memset(&opts, 0, sizeof(opts)); + opts.imtu = HIDP_DEFAULT_MTU; + opts.omtu = HIDP_DEFAULT_MTU; + opts.flush_to = 0xffff; + + setsockopt(sk, SOL_L2CAP, L2CAP_OPTIONS, &opts, sizeof(opts)); + + if (listen(sk, backlog) < 0) { + close(sk); + return -1; + } + + return sk; +} + +static int l2cap_accept(int sk, bdaddr_t *bdaddr) +{ + struct sockaddr_l2 addr; + socklen_t addrlen; + int nsk; + + memset(&addr, 0, sizeof(addr)); + addrlen = sizeof(addr); + + if ((nsk = accept(sk, (struct sockaddr *) &addr, &addrlen)) < 0) + return -1; + + if (bdaddr) + bacpy(bdaddr, &addr.l2_bdaddr); + + return nsk; +} + +static int request_authentication(bdaddr_t *src, bdaddr_t *dst) +{ + struct hci_conn_info_req *cr; + char addr[18]; + int err, dd, dev_id; + + ba2str(src, addr); + dev_id = hci_devid(addr); + if (dev_id < 0) + return dev_id; + + dd = hci_open_dev(dev_id); + if (dd < 0) + return dd; + + cr = malloc(sizeof(*cr) + sizeof(struct hci_conn_info)); + if (!cr) + return -ENOMEM; + + bacpy(&cr->bdaddr, dst); + cr->type = ACL_LINK; + err = ioctl(dd, HCIGETCONNINFO, (unsigned long) cr); + if (err < 0) { + free(cr); + hci_close_dev(dd); + return err; + } + + err = hci_authenticate_link(dd, htobs(cr->conn_info->handle), 25000); + + free(cr); + hci_close_dev(dd); + + return err; +} + +static int request_encryption(bdaddr_t *src, bdaddr_t *dst) +{ + struct hci_conn_info_req *cr; + char addr[18]; + int err, dd, dev_id; + + ba2str(src, addr); + dev_id = hci_devid(addr); + if (dev_id < 0) + return dev_id; + + dd = hci_open_dev(dev_id); + if (dd < 0) + return dd; + + cr = malloc(sizeof(*cr) + sizeof(struct hci_conn_info)); + if (!cr) + return -ENOMEM; + + bacpy(&cr->bdaddr, dst); + cr->type = ACL_LINK; + err = ioctl(dd, HCIGETCONNINFO, (unsigned long) cr); + if (err < 0) { + free(cr); + hci_close_dev(dd); + return err; + } + + err = hci_encrypt_link(dd, htobs(cr->conn_info->handle), 1, 25000); + + free(cr); + hci_close_dev(dd); + + return err; +} + +static int create_device(int ctl, int csk, int isk, uint8_t subclass, int nosdp, int nocheck, int bootonly, int encrypt, int timeout) +{ + struct hidp_connadd_req req; + struct sockaddr_l2 addr; + socklen_t addrlen; + bdaddr_t src, dst; + char bda[18]; + int err; + + memset(&addr, 0, sizeof(addr)); + addrlen = sizeof(addr); + + if (getsockname(csk, (struct sockaddr *) &addr, &addrlen) < 0) + return -1; + + bacpy(&src, &addr.l2_bdaddr); + + memset(&addr, 0, sizeof(addr)); + addrlen = sizeof(addr); + + if (getpeername(csk, (struct sockaddr *) &addr, &addrlen) < 0) + return -1; + + bacpy(&dst, &addr.l2_bdaddr); + + memset(&req, 0, sizeof(req)); + req.ctrl_sock = csk; + req.intr_sock = isk; + req.flags = 0; + req.idle_to = timeout * 60; + + err = get_stored_device_info(&src, &dst, &req); + if (!err) + goto create; + + if (!nocheck) { + ba2str(&dst, bda); + syslog(LOG_ERR, "Rejected connection from unknown device %s", bda); + /* Return no error to avoid run_server() complaining too */ + return 0; + } + + if (!nosdp) { + err = get_sdp_device_info(&src, &dst, &req); + if (err < 0) + goto error; + } else { + struct l2cap_conninfo conn; + socklen_t size; + uint8_t class[3]; + + memset(&conn, 0, sizeof(conn)); + size = sizeof(conn); + if (getsockopt(csk, SOL_L2CAP, L2CAP_CONNINFO, &conn, &size) < 0) + memset(class, 0, 3); + else + memcpy(class, conn.dev_class, 3); + + if (class[1] == 0x25 && (class[2] == 0x00 || class[2] == 0x01)) + req.subclass = class[0]; + else + req.subclass = 0xc0; + } + +create: + if (subclass != 0x00) + req.subclass = subclass; + + ba2str(&dst, bda); + syslog(LOG_INFO, "New HID device %s (%s)", bda, req.name); + + if (encrypt && (req.subclass & 0x40)) { + err = request_authentication(&src, &dst); + if (err < 0) { + syslog(LOG_ERR, "Authentication for %s failed", bda); + goto error; + } + + err = request_encryption(&src, &dst); + if (err < 0) + syslog(LOG_ERR, "Encryption for %s failed", bda); + } + + if (bootonly) { + req.rd_size = 0; + req.flags |= (1 << HIDP_BOOT_PROTOCOL_MODE); + } + + err = ioctl(ctl, HIDPCONNADD, &req); + +error: + free(req.rd_data); + + return err; +} + +static void run_server(int ctl, int csk, int isk, uint8_t subclass, int nosdp, int nocheck, int bootonly, int encrypt, int timeout) +{ + struct pollfd p[2]; + sigset_t sigs; + short events; + int err, ncsk, nisk; + + sigfillset(&sigs); + sigdelset(&sigs, SIGCHLD); + sigdelset(&sigs, SIGPIPE); + sigdelset(&sigs, SIGTERM); + sigdelset(&sigs, SIGINT); + sigdelset(&sigs, SIGHUP); + + p[0].fd = csk; + p[0].events = POLLIN | POLLERR | POLLHUP; + + p[1].fd = isk; + p[1].events = POLLIN | POLLERR | POLLHUP; + + while (!__io_canceled) { + p[0].revents = 0; + p[1].revents = 0; + + if (ppoll(p, 2, NULL, &sigs) < 1) + continue; + + events = p[0].revents | p[1].revents; + + if (events & POLLIN) { + ncsk = l2cap_accept(csk, NULL); + nisk = l2cap_accept(isk, NULL); + + err = create_device(ctl, ncsk, nisk, subclass, nosdp, nocheck, bootonly, encrypt, timeout); + if (err < 0) + syslog(LOG_ERR, "HID create error %d (%s)", + errno, strerror(errno)); + + close(nisk); + sleep(1); + close(ncsk); + } + } +} + +static char *hidp_state[] = { + "unknown", + "connected", + "open", + "bound", + "listening", + "connecting", + "connecting", + "config", + "disconnecting", + "closed" +}; + +static char *hidp_flagstostr(uint32_t flags) +{ + static char str[100]; + str[0] = 0; + + strcat(str, "["); + + if (flags & (1 << HIDP_BOOT_PROTOCOL_MODE)) + strcat(str, "boot-protocol"); + + strcat(str, "]"); + + return str; +} + +static void do_show(int ctl) +{ + struct hidp_connlist_req req; + struct hidp_conninfo ci[16]; + char addr[18]; + unsigned int i; + + req.cnum = 16; + req.ci = ci; + + if (ioctl(ctl, HIDPGETCONNLIST, &req) < 0) { + perror("Can't get connection list"); + close(ctl); + exit(1); + } + + for (i = 0; i < req.cnum; i++) { + ba2str(&ci[i].bdaddr, addr); + printf("%s %s [%04x:%04x] %s %s\n", addr, ci[i].name, + ci[i].vendor, ci[i].product, hidp_state[ci[i].state], + ci[i].flags ? hidp_flagstostr(ci[i].flags) : ""); + } +} + +static void do_connect(int ctl, bdaddr_t *src, bdaddr_t *dst, uint8_t subclass, int fakehid, int bootonly, int encrypt, int timeout) +{ + struct hidp_connadd_req req; + uint16_t uuid = HID_SVCLASS_ID; + uint8_t channel = 0; + char name[256]; + int csk, isk, err; + + memset(&req, 0, sizeof(req)); + name[0] = '\0'; + + err = get_sdp_device_info(src, dst, &req); + if (err < 0 && fakehid) + err = get_alternate_device_info(src, dst, + &uuid, &channel, name, sizeof(name) - 1); + + if (err < 0) { + perror("Can't get device information"); + close(ctl); + exit(1); + } + + switch (uuid) { + case HID_SVCLASS_ID: + goto connect; + + case SERIAL_PORT_SVCLASS_ID: + if (subclass == 0x40 || !strcmp(name, "Cable Replacement")) { + if (epox_presenter(src, dst, channel) < 0) { + close(ctl); + exit(1); + } + break; + } + if (subclass == 0x1f || !strcmp(name, "SPP slave")) { + if (jthree_keyboard(src, dst, channel) < 0) { + close(ctl); + exit(1); + } + break; + } + if (subclass == 0x02 || !strcmp(name, "Serial Port")) { + if (celluon_keyboard(src, dst, channel) < 0) { + close(ctl); + exit(1); + } + break; + } + break; + + case HEADSET_SVCLASS_ID: + case HANDSFREE_SVCLASS_ID: + if (headset_presenter(src, dst, channel) < 0) { + close(ctl); + exit(1); + } + break; + } + + return; + +connect: + csk = l2cap_connect(src, dst, L2CAP_PSM_HIDP_CTRL); + if (csk < 0) { + perror("Can't create HID control channel"); + close(ctl); + exit(1); + } + + isk = l2cap_connect(src, dst, L2CAP_PSM_HIDP_INTR); + if (isk < 0) { + perror("Can't create HID interrupt channel"); + close(csk); + close(ctl); + exit(1); + } + + err = create_device(ctl, csk, isk, subclass, 1, 1, bootonly, encrypt, timeout); + if (err < 0) { + fprintf(stderr, "HID create error %d (%s)\n", + errno, strerror(errno)); + close(isk); + sleep(1); + close(csk); + close(ctl); + exit(1); + } +} + +static void do_search(int ctl, bdaddr_t *bdaddr, uint8_t subclass, int fakehid, int bootonly, int encrypt, int timeout) +{ + inquiry_info *info = NULL; + bdaddr_t src, dst; + int i, dev_id, num_rsp, length, flags; + char addr[18]; + uint8_t class[3]; + + ba2str(bdaddr, addr); + dev_id = hci_devid(addr); + if (dev_id < 0) { + dev_id = hci_get_route(NULL); + hci_devba(dev_id, &src); + } else + bacpy(&src, bdaddr); + + length = 8; /* ~10 seconds */ + num_rsp = 0; + flags = IREQ_CACHE_FLUSH; + + printf("Searching ...\n"); + + num_rsp = hci_inquiry(dev_id, length, num_rsp, NULL, &info, flags); + + for (i = 0; i < num_rsp; i++) { + memcpy(class, (info+i)->dev_class, 3); + if (class[1] == 0x25 && (class[2] == 0x00 || class[2] == 0x01)) { + bacpy(&dst, &(info+i)->bdaddr); + ba2str(&dst, addr); + + printf("\tConnecting to device %s\n", addr); + do_connect(ctl, &src, &dst, subclass, fakehid, bootonly, encrypt, timeout); + } + } + + if (!fakehid) + goto done; + + for (i = 0; i < num_rsp; i++) { + memcpy(class, (info+i)->dev_class, 3); + if ((class[0] == 0x00 && class[2] == 0x00 && + (class[1] == 0x40 || class[1] == 0x1f)) || + (class[0] == 0x10 && class[1] == 0x02 && class[2] == 0x40)) { + bacpy(&dst, &(info+i)->bdaddr); + ba2str(&dst, addr); + + printf("\tConnecting to device %s\n", addr); + do_connect(ctl, &src, &dst, subclass, 1, bootonly, 0, timeout); + } + } + +done: + bt_free(info); + + if (!num_rsp) { + fprintf(stderr, "\tNo devices in range or visible\n"); + close(ctl); + exit(1); + } +} + +static void do_kill(int ctl, bdaddr_t *bdaddr, uint32_t flags) +{ + struct hidp_conndel_req req; + struct hidp_connlist_req cl; + struct hidp_conninfo ci[16]; + unsigned int i; + + if (!bacmp(bdaddr, BDADDR_ALL)) { + cl.cnum = 16; + cl.ci = ci; + + if (ioctl(ctl, HIDPGETCONNLIST, &cl) < 0) { + perror("Can't get connection list"); + close(ctl); + exit(1); + } + + for (i = 0; i < cl.cnum; i++) { + bacpy(&req.bdaddr, &ci[i].bdaddr); + req.flags = flags; + + if (ioctl(ctl, HIDPCONNDEL, &req) < 0) { + perror("Can't release connection"); + close(ctl); + exit(1); + } + } + + } else { + bacpy(&req.bdaddr, bdaddr); + req.flags = flags; + + if (ioctl(ctl, HIDPCONNDEL, &req) < 0) { + perror("Can't release connection"); + close(ctl); + exit(1); + } + } +} + +static void usage(void) +{ + printf("hidd - Bluetooth HID daemon version %s\n\n", VERSION); + + printf("Usage:\n" + "\thidd [options] [commands]\n" + "\n"); + + printf("Options:\n" + "\t-i Local HCI device or BD Address\n" + "\t-t Set idle timeout (in minutes)\n" + "\t-b Overwrite the boot mode subclass\n" + "\t-n, --nodaemon Don't fork daemon to background\n" + "\t-h, --help Display help\n" + "\n"); + + printf("Commands:\n" + "\t--server Start HID server\n" + "\t--search Search for HID devices\n" + "\t--connect Connect remote HID device\n" + "\t--unplug Unplug the HID connection\n" + "\t--kill Terminate HID connection\n" + "\t--killall Terminate all connections\n" + "\t--show List current HID connections\n" + "\n"); +} + +static struct option main_options[] = { + { "help", 0, 0, 'h' }, + { "nodaemon", 0, 0, 'n' }, + { "subclass", 1, 0, 'b' }, + { "timeout", 1, 0, 't' }, + { "device", 1, 0, 'i' }, + { "master", 0, 0, 'M' }, + { "encrypt", 0, 0, 'E' }, + { "nosdp", 0, 0, 'D' }, + { "nocheck", 0, 0, 'Z' }, + { "bootonly", 0, 0, 'B' }, + { "hidonly", 0, 0, 'H' }, + { "show", 0, 0, 'l' }, + { "list", 0, 0, 'l' }, + { "server", 0, 0, 'd' }, + { "listen", 0, 0, 'd' }, + { "search", 0, 0, 's' }, + { "create", 1, 0, 'c' }, + { "connect", 1, 0, 'c' }, + { "disconnect", 1, 0, 'k' }, + { "terminate", 1, 0, 'k' }, + { "release", 1, 0, 'k' }, + { "kill", 1, 0, 'k' }, + { "killall", 0, 0, 'K' }, + { "unplug", 1, 0, 'u' }, + { 0, 0, 0, 0 } +}; + +int main(int argc, char *argv[]) +{ + struct sigaction sa; + bdaddr_t bdaddr, dev; + uint32_t flags = 0; + uint8_t subclass = 0x00; + char addr[18]; + int log_option = LOG_NDELAY | LOG_PID; + int opt, ctl, csk, isk; + int mode = SHOW, detach = 1, nosdp = 0, nocheck = 0, bootonly = 0; + int fakehid = 1, encrypt = 0, timeout = 30, lm = 0; + + bacpy(&bdaddr, BDADDR_ANY); + + while ((opt = getopt_long(argc, argv, "+i:nt:b:MEDZBHldsc:k:Ku:h", main_options, NULL)) != -1) { + switch(opt) { + case 'i': + if (!strncasecmp(optarg, "hci", 3)) + hci_devba(atoi(optarg + 3), &bdaddr); + else + str2ba(optarg, &bdaddr); + break; + case 'n': + detach = 0; + break; + case 't': + timeout = atoi(optarg); + break; + case 'b': + if (!strncasecmp(optarg, "0x", 2)) + subclass = (uint8_t) strtol(optarg, NULL, 16); + else + subclass = atoi(optarg); + break; + case 'M': + lm |= L2CAP_LM_MASTER; + break; + case 'E': + encrypt = 1; + break; + case 'D': + nosdp = 1; + break; + case 'Z': + nocheck = 1; + break; + case 'B': + bootonly = 1; + break; + case 'H': + fakehid = 0; + break; + case 'l': + mode = SHOW; + break; + case 'd': + mode = SERVER; + break; + case 's': + mode = SEARCH; + break; + case 'c': + str2ba(optarg, &dev); + mode = CONNECT; + break; + case 'k': + str2ba(optarg, &dev); + mode = KILL; + break; + case 'K': + bacpy(&dev, BDADDR_ALL); + mode = KILL; + break; + case 'u': + str2ba(optarg, &dev); + flags = (1 << HIDP_VIRTUAL_CABLE_UNPLUG); + mode = KILL; + break; + case 'h': + usage(); + exit(0); + default: + exit(0); + } + } + + ba2str(&bdaddr, addr); + + ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HIDP); + if (ctl < 0) { + perror("Can't open HIDP control socket"); + exit(1); + } + + switch (mode) { + case SERVER: + csk = l2cap_listen(&bdaddr, L2CAP_PSM_HIDP_CTRL, lm, 10); + if (csk < 0) { + perror("Can't listen on HID control channel"); + close(ctl); + exit(1); + } + + isk = l2cap_listen(&bdaddr, L2CAP_PSM_HIDP_INTR, lm, 10); + if (isk < 0) { + perror("Can't listen on HID interrupt channel"); + close(ctl); + close(csk); + exit(1); + } + break; + + case SEARCH: + do_search(ctl, &bdaddr, subclass, fakehid, bootonly, encrypt, timeout); + close(ctl); + exit(0); + + case CONNECT: + do_connect(ctl, &bdaddr, &dev, subclass, fakehid, bootonly, encrypt, timeout); + close(ctl); + exit(0); + + case KILL: + do_kill(ctl, &dev, flags); + close(ctl); + exit(0); + + default: + do_show(ctl); + close(ctl); + exit(0); + } + + if (detach) { + if (daemon(0, 0)) { + perror("Can't start daemon"); + exit(1); + } + } else + log_option |= LOG_PERROR; + + openlog("hidd", log_option, LOG_DAEMON); + + if (bacmp(&bdaddr, BDADDR_ANY)) + syslog(LOG_INFO, "Bluetooth HID daemon (%s)", addr); + else + syslog(LOG_INFO, "Bluetooth HID daemon"); + + memset(&sa, 0, sizeof(sa)); + sa.sa_flags = SA_NOCLDSTOP; + + sa.sa_handler = sig_term; + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + sa.sa_handler = sig_hup; + sigaction(SIGHUP, &sa, NULL); + + sa.sa_handler = SIG_IGN; + sigaction(SIGCHLD, &sa, NULL); + sigaction(SIGPIPE, &sa, NULL); + + run_server(ctl, csk, isk, subclass, nosdp, nocheck, bootonly, encrypt, timeout); + + syslog(LOG_INFO, "Exit"); + + close(csk); + close(isk); + close(ctl); + + return 0; +} diff --git a/compat/hidd.h b/compat/hidd.h new file mode 100644 index 0000000..0536967 --- /dev/null +++ b/compat/hidd.h @@ -0,0 +1,30 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2003-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#define L2CAP_PSM_HIDP_CTRL 0x11 +#define L2CAP_PSM_HIDP_INTR 0x13 + +int epox_presenter(const bdaddr_t *src, const bdaddr_t *dst, uint8_t channel); +int headset_presenter(const bdaddr_t *src, const bdaddr_t *dst, uint8_t channel); +int jthree_keyboard(const bdaddr_t *src, const bdaddr_t *dst, uint8_t channel); +int celluon_keyboard(const bdaddr_t *src, const bdaddr_t *dst, uint8_t channel); diff --git a/compat/lib.h b/compat/lib.h new file mode 100644 index 0000000..3b3aeb5 --- /dev/null +++ b/compat/lib.h @@ -0,0 +1,86 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2002-2003 Maxim Krasnyansky + * Copyright (C) 2002-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include +#include + +#ifndef min +#define min(a,b) ( (a)<(b) ? (a):(b) ) +#endif + +/* IO cancelation */ +extern volatile sig_atomic_t __io_canceled; + +static inline void io_init(void) +{ + __io_canceled = 0; +} + +static inline void io_cancel(void) +{ + __io_canceled = 1; +} + +/* Read exactly len bytes (Signal safe)*/ +static inline int read_n(int fd, char *buf, int len) +{ + register int t = 0, w; + + while (!__io_canceled && len > 0) { + if ((w = read(fd, buf, len)) < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + return -1; + } + if (!w) + return 0; + len -= w; + buf += w; + t += w; + } + + return t; +} + +/* Write exactly len bytes (Signal safe)*/ +static inline int write_n(int fd, char *buf, int len) +{ + register int t = 0, w; + + while (!__io_canceled && len > 0) { + if ((w = write(fd, buf, len)) < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + return -1; + } + if (!w) + return 0; + len -= w; + buf += w; + t += w; + } + + return t; +} diff --git a/compat/msdun.c b/compat/msdun.c new file mode 100644 index 0000000..ae88c0c --- /dev/null +++ b/compat/msdun.c @@ -0,0 +1,153 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2002-2003 Maxim Krasnyansky + * Copyright (C) 2002-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lib.h" +#include "dund.h" + +#define MS_PPP 2 +#define MS_SUCCESS 1 +#define MS_FAILED -1 +#define MS_TIMEOUT -2 + +static sigjmp_buf jmp; +static int retry; +static int timeout; + +static void sig_alarm(int sig) +{ + siglongjmp(jmp, MS_TIMEOUT); +} + +static int w4_str(int fd, char *str) +{ + char buf[40]; + unsigned len = 0; + int r; + + while (1) { + r = read(fd, buf + len, sizeof(buf) - len - 1); + if (r < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + break; + } + if (!r) + break; + + len += r; + + if (len < strlen(str)) + continue; + buf[len] = 0; + + if (strstr(buf, str)) + return MS_SUCCESS; + + /* Detect PPP */ + if (strchr(buf, '~')) + return MS_PPP; + } + return MS_FAILED; +} + +static int ms_server(int fd) +{ + switch (w4_str(fd, "CLIENT")) { + case MS_SUCCESS: + write_n(fd, "CLIENTSERVER", 12); + case MS_PPP: + return MS_SUCCESS; + default: + return MS_FAILED; + } +} + +static int ms_client(int fd) +{ + write_n(fd, "CLIENT", 6); + return w4_str(fd, "CLIENTSERVER"); +} + +int ms_dun(int fd, int server, int timeo) +{ + sig_t osig; + + retry = 4; + timeout = timeo; + + if (!server) + timeout /= retry; + + osig = signal(SIGALRM, sig_alarm); + + while (1) { + int r = sigsetjmp(jmp, 1); + if (r) { + if (r == MS_TIMEOUT && !server && --retry) + continue; + + alarm(0); + signal(SIGALRM, osig); + + switch (r) { + case MS_SUCCESS: + case MS_PPP: + errno = 0; + return 0; + + case MS_FAILED: + errno = EPROTO; + break; + + case MS_TIMEOUT: + errno = ETIMEDOUT; + break; + } + return -1; + } + + alarm(timeout); + + if (server) + r = ms_server(fd); + else + r = ms_client(fd); + + siglongjmp(jmp, r); + } +} diff --git a/compat/pand.1 b/compat/pand.1 new file mode 100644 index 0000000..4603b8b --- /dev/null +++ b/compat/pand.1 @@ -0,0 +1,77 @@ +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.29. +.TH BlueZ "1" "February 2003" "PAN daemon" "User Commands" +.SH NAME +pand \- BlueZ Bluetooth PAN daemon +.SH DESCRIPTION +The pand PAN daemon allows your computer to connect to ethernet +networks using Bluetooth. +.SH SYNPOSIS +pand +.SH OPTIONS +.TP +\fB\-\-show\fR \fB\-\-list\fR \fB\-l\fR +Show active PAN connections +.TP +\fB\-\-listen\fR \fB\-s\fR +Listen for PAN connections +.TP +\fB\-\-connect\fR \fB\-c\fR +Create PAN connection +.TP +\fB\-\-search\fR \fB\-Q[duration]\fR +Search and connect +.TP +\fB\-\-kill\fR \fB\-k\fR +Kill PAN connection +.TP +\fB\-\-killall\fR \fB\-K\fR +Kill all PAN connections +.TP +\fB\-\-role\fR \fB\-r\fR +Local PAN role (PANU, NAP, GN) +.TP +\fB\-\-service\fR \fB\-d\fR +Remote PAN service (PANU, NAP, GN) +.TP +\fB\-\-ethernet\fR \fB\-e\fR +Network interface name +.TP +\fB\-\-device\fR \fB\-i\fR +Source bdaddr +.TP +\fB\-\-nosdp\fR \fB\-D\fR +Disable SDP +.TP +\fB\-\-encrypt\fR \fB\-E\fR +Enable encryption +.TP +\fB\-\-secure\fR \fB\-S\fR +Secure connection +.TP +\fB\-\-master\fR \fB\-M\fR +Become the master of a piconet +.TP +\fB\-\-nodetach\fR \fB\-n\fR +Do not become a daemon +.TP +\fB\-\-persist\fR \fB\-p[interval]\fR +Persist mode +.TP +\fB\-\-cache\fR \fB\-C[valid]\fR +Cache addresses +.TP +\fB\-\-pidfile\fR \fB\-P \fR +Create PID file +.TP +\fB\-\-devup\fR \fB\-u