├── .gitignore ├── AndroidManifest.xml ├── NOTICE ├── README ├── res ├── drawable-hdpi │ ├── background.png │ ├── background_key.png │ ├── background_number.png │ ├── background_vumeter.png │ ├── btn_delete_disabled.9.png │ ├── btn_delete_normal.9.png │ ├── btn_delete_pressed.9.png │ ├── btn_finish_disabled.9.png │ ├── btn_finish_normal.9.png │ ├── btn_finish_pressed.9.png │ ├── btn_new_disabled.9.png │ ├── btn_new_normal.9.png │ ├── btn_new_pressed.9.png │ ├── btn_pause_normal.9.png │ ├── btn_pause_pressed.9.png │ ├── btn_play_normal.9.png │ ├── btn_play_pressed.9.png │ ├── btn_record_normal.9.png │ ├── btn_record_pressed.9.png │ ├── btn_stop_normal.9.png │ ├── btn_stop_pressed.9.png │ ├── colon.png │ ├── empty_icon.png │ ├── folder.png │ ├── ic_launcher_soundrecorder.png │ ├── ic_menu_fm.png │ ├── ic_menu_view_list.png │ ├── icon_vumeter.png │ ├── number_0.png │ ├── number_1.png │ ├── number_2.png │ ├── number_3.png │ ├── number_4.png │ ├── number_5.png │ ├── number_6.png │ ├── number_7.png │ ├── number_8.png │ ├── number_9.png │ ├── play.png │ ├── record.png │ ├── stat_sys_call_record.png │ ├── stat_sys_call_record_full.png │ ├── stop.png │ ├── tape_bottom.png │ ├── tape_top.png │ ├── wheel_left.png │ ├── wheel_right.png │ ├── wheel_small_left.png │ └── wheel_small_right.png ├── drawable │ ├── btn_delete.xml │ ├── btn_finish.xml │ ├── btn_new.xml │ ├── btn_pause.xml │ ├── btn_play.xml │ ├── btn_record.xml │ └── btn_stop.xml ├── layout │ ├── context_menu_header.xml │ ├── main.xml │ └── view_list_menu.xml ├── values-zh-rCN │ └── strings.xml ├── values │ ├── arrays.xml │ └── strings.xml └── xml │ └── preferences.xml └── src └── net └── micode └── soundrecorder ├── RecordNameEditText.java ├── Recorder.java ├── RecorderService.java ├── RemainingTimeCalculator.java ├── SeamlessAnimation.java ├── SoundRecorder.java ├── SoundRecorderPreferenceActivity.java └── WheelImageView.java /.gitignore: -------------------------------------------------------------------------------- 1 | # generated files 2 | bin/ 3 | gen/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | proguard.cfg 7 | project.properties 8 | .settings/ 9 | .classpath 10 | .project 11 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 66 | 67 | 68 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | 13 | 14 | Apache License 15 | Version 2.0, January 2004 16 | http://www.apache.org/licenses/ 17 | 18 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 19 | 20 | 1. Definitions. 21 | 22 | "License" shall mean the terms and conditions for use, reproduction, 23 | and distribution as defined by Sections 1 through 9 of this document. 24 | 25 | "Licensor" shall mean the copyright owner or entity authorized by 26 | the copyright owner that is granting the License. 27 | 28 | "Legal Entity" shall mean the union of the acting entity and all 29 | other entities that control, are controlled by, or are under common 30 | control with that entity. For the purposes of this definition, 31 | "control" means (i) the power, direct or indirect, to cause the 32 | direction or management of such entity, whether by contract or 33 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 34 | outstanding shares, or (iii) beneficial ownership of such entity. 35 | 36 | "You" (or "Your") shall mean an individual or Legal Entity 37 | exercising permissions granted by this License. 38 | 39 | "Source" form shall mean the preferred form for making modifications, 40 | including but not limited to software source code, documentation 41 | source, and configuration files. 42 | 43 | "Object" form shall mean any form resulting from mechanical 44 | transformation or translation of a Source form, including but 45 | not limited to compiled object code, generated documentation, 46 | and conversions to other media types. 47 | 48 | "Work" shall mean the work of authorship, whether in Source or 49 | Object form, made available under the License, as indicated by a 50 | copyright notice that is included in or attached to the work 51 | (an example is provided in the Appendix below). 52 | 53 | "Derivative Works" shall mean any work, whether in Source or Object 54 | form, that is based on (or derived from) the Work and for which the 55 | editorial revisions, annotations, elaborations, or other modifications 56 | represent, as a whole, an original work of authorship. For the purposes 57 | of this License, Derivative Works shall not include works that remain 58 | separable from, or merely link (or bind by name) to the interfaces of, 59 | the Work and Derivative Works thereof. 60 | 61 | "Contribution" shall mean any work of authorship, including 62 | the original version of the Work and any modifications or additions 63 | to that Work or Derivative Works thereof, that is intentionally 64 | submitted to Licensor for inclusion in the Work by the copyright owner 65 | or by an individual or Legal Entity authorized to submit on behalf of 66 | the copyright owner. For the purposes of this definition, "submitted" 67 | means any form of electronic, verbal, or written communication sent 68 | to the Licensor or its representatives, including but not limited to 69 | communication on electronic mailing lists, source code control systems, 70 | and issue tracking systems that are managed by, or on behalf of, the 71 | Licensor for the purpose of discussing and improving the Work, but 72 | excluding communication that is conspicuously marked or otherwise 73 | designated in writing by the copyright owner as "Not a Contribution." 74 | 75 | "Contributor" shall mean Licensor and any individual or Legal Entity 76 | on behalf of whom a Contribution has been received by Licensor and 77 | subsequently incorporated within the Work. 78 | 79 | 2. Grant of Copyright License. Subject to the terms and conditions of 80 | this License, each Contributor hereby grants to You a perpetual, 81 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 82 | copyright license to reproduce, prepare Derivative Works of, 83 | publicly display, publicly perform, sublicense, and distribute the 84 | Work and such Derivative Works in Source or Object form. 85 | 86 | 3. Grant of Patent License. Subject to the terms and conditions of 87 | this License, each Contributor hereby grants to You a perpetual, 88 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 89 | (except as stated in this section) patent license to make, have made, 90 | use, offer to sell, sell, import, and otherwise transfer the Work, 91 | where such license applies only to those patent claims licensable 92 | by such Contributor that are necessarily infringed by their 93 | Contribution(s) alone or by combination of their Contribution(s) 94 | with the Work to which such Contribution(s) was submitted. If You 95 | institute patent litigation against any entity (including a 96 | cross-claim or counterclaim in a lawsuit) alleging that the Work 97 | or a Contribution incorporated within the Work constitutes direct 98 | or contributory patent infringement, then any patent licenses 99 | granted to You under this License for that Work shall terminate 100 | as of the date such litigation is filed. 101 | 102 | 4. Redistribution. You may reproduce and distribute copies of the 103 | Work or Derivative Works thereof in any medium, with or without 104 | modifications, and in Source or Object form, provided that You 105 | meet the following conditions: 106 | 107 | (a) You must give any other recipients of the Work or 108 | Derivative Works a copy of this License; and 109 | 110 | (b) You must cause any modified files to carry prominent notices 111 | stating that You changed the files; and 112 | 113 | (c) You must retain, in the Source form of any Derivative Works 114 | that You distribute, all copyright, patent, trademark, and 115 | attribution notices from the Source form of the Work, 116 | excluding those notices that do not pertain to any part of 117 | the Derivative Works; and 118 | 119 | (d) If the Work includes a "NOTICE" text file as part of its 120 | distribution, then any Derivative Works that You distribute must 121 | include a readable copy of the attribution notices contained 122 | within such NOTICE file, excluding those notices that do not 123 | pertain to any part of the Derivative Works, in at least one 124 | of the following places: within a NOTICE text file distributed 125 | as part of the Derivative Works; within the Source form or 126 | documentation, if provided along with the Derivative Works; or, 127 | within a display generated by the Derivative Works, if and 128 | wherever such third-party notices normally appear. The contents 129 | of the NOTICE file are for informational purposes only and 130 | do not modify the License. You may add Your own attribution 131 | notices within Derivative Works that You distribute, alongside 132 | or as an addendum to the NOTICE text from the Work, provided 133 | that such additional attribution notices cannot be construed 134 | as modifying the License. 135 | 136 | You may add Your own copyright statement to Your modifications and 137 | may provide additional or different license terms and conditions 138 | for use, reproduction, or distribution of Your modifications, or 139 | for any such Derivative Works as a whole, provided Your use, 140 | reproduction, and distribution of the Work otherwise complies with 141 | the conditions stated in this License. 142 | 143 | 5. Submission of Contributions. Unless You explicitly state otherwise, 144 | any Contribution intentionally submitted for inclusion in the Work 145 | by You to the Licensor shall be under the terms and conditions of 146 | this License, without any additional terms or conditions. 147 | Notwithstanding the above, nothing herein shall supersede or modify 148 | the terms of any separate license agreement you may have executed 149 | with Licensor regarding such Contributions. 150 | 151 | 6. Trademarks. This License does not grant permission to use the trade 152 | names, trademarks, service marks, or product names of the Licensor, 153 | except as required for reasonable and customary use in describing the 154 | origin of the Work and reproducing the content of the NOTICE file. 155 | 156 | 7. Disclaimer of Warranty. Unless required by applicable law or 157 | agreed to in writing, Licensor provides the Work (and each 158 | Contributor provides its Contributions) on an "AS IS" BASIS, 159 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 160 | implied, including, without limitation, any warranties or conditions 161 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 162 | PARTICULAR PURPOSE. You are solely responsible for determining the 163 | appropriateness of using or redistributing the Work and assume any 164 | risks associated with Your exercise of permissions under this License. 165 | 166 | 8. Limitation of Liability. In no event and under no legal theory, 167 | whether in tort (including negligence), contract, or otherwise, 168 | unless required by applicable law (such as deliberate and grossly 169 | negligent acts) or agreed to in writing, shall any Contributor be 170 | liable to You for damages, including any direct, indirect, special, 171 | incidental, or consequential damages of any character arising as a 172 | result of this License or out of the use or inability to use the 173 | Work (including but not limited to damages for loss of goodwill, 174 | work stoppage, computer failure or malfunction, or any and all 175 | other commercial damages or losses), even if such Contributor 176 | has been advised of the possibility of such damages. 177 | 178 | 9. Accepting Warranty or Additional Liability. While redistributing 179 | the Work or Derivative Works thereof, You may choose to offer, 180 | and charge a fee for, acceptance of support, warranty, indemnity, 181 | or other liability obligations and/or rights consistent with this 182 | License. However, in accepting such obligations, You may act only 183 | on Your own behalf and on Your sole responsibility, not on behalf 184 | of any other Contributor, and only if You agree to indemnify, 185 | defend, and hold each Contributor harmless for any liability 186 | incurred by, or claims asserted against, such Contributor by reason 187 | of your accepting any such warranty or additional liability. 188 | 189 | END OF TERMS AND CONDITIONS 190 | 191 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | [中文] 2 | 3 | 1. 本项目是MIUI录音机的社区开源版,由MIUI团队(www.miui.com) 发起并贡献第一批代码,遵循NOTICE文件所描述的开源协议, 4 | 今后为MiCode社区(www.micode.net) 拥有,并由社区发布和维护。 5 | 6 | 2. Bug反馈和跟踪,请访问Github, 7 | https://github.com/MiCode/SoundRecorder/issues?sort=created&direction=desc&state=open 8 | 9 | 3. 功能建议和综合讨论,请访问MiCode, 10 | http://micode.net/forum-39-1.html 11 | 12 | 13 | [English] 14 | 15 | 1. This is open source edition of MIUI SoundRecorder, it's first initiated and sponsored by MIUI team (www.miui.com). 16 | It's opened under license described by NOTICE file. It's owned by the MiCode community (www.micode.net). In future, 17 | the MiCode community will release and maintain this project. 18 | 19 | 2. Regarding issue tracking, please visit Github, 20 | https://github.com/MiCode/SoundRecorder/issues?sort=created&direction=desc&state=open 21 | 22 | 3. Regarding feature request and general discussion, please visit Micode forum, 23 | http://micode.net/forum-39-1.html 24 | -------------------------------------------------------------------------------- /res/drawable-hdpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/background.png -------------------------------------------------------------------------------- /res/drawable-hdpi/background_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/background_key.png -------------------------------------------------------------------------------- /res/drawable-hdpi/background_number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/background_number.png -------------------------------------------------------------------------------- /res/drawable-hdpi/background_vumeter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/background_vumeter.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_delete_disabled.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_delete_disabled.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_delete_normal.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_delete_normal.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_delete_pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_delete_pressed.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_finish_disabled.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_finish_disabled.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_finish_normal.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_finish_normal.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_finish_pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_finish_pressed.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_new_disabled.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_new_disabled.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_new_normal.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_new_normal.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_new_pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_new_pressed.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_pause_normal.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_pause_normal.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_pause_pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_pause_pressed.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_play_normal.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_play_normal.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_play_pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_play_pressed.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_record_normal.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_record_normal.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_record_pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_record_pressed.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_stop_normal.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_stop_normal.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/btn_stop_pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/btn_stop_pressed.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/colon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/colon.png -------------------------------------------------------------------------------- /res/drawable-hdpi/empty_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/empty_icon.png -------------------------------------------------------------------------------- /res/drawable-hdpi/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/folder.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_launcher_soundrecorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/ic_launcher_soundrecorder.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_menu_fm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/ic_menu_fm.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_menu_view_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/ic_menu_view_list.png -------------------------------------------------------------------------------- /res/drawable-hdpi/icon_vumeter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/icon_vumeter.png -------------------------------------------------------------------------------- /res/drawable-hdpi/number_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/number_0.png -------------------------------------------------------------------------------- /res/drawable-hdpi/number_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/number_1.png -------------------------------------------------------------------------------- /res/drawable-hdpi/number_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/number_2.png -------------------------------------------------------------------------------- /res/drawable-hdpi/number_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/number_3.png -------------------------------------------------------------------------------- /res/drawable-hdpi/number_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/number_4.png -------------------------------------------------------------------------------- /res/drawable-hdpi/number_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/number_5.png -------------------------------------------------------------------------------- /res/drawable-hdpi/number_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/number_6.png -------------------------------------------------------------------------------- /res/drawable-hdpi/number_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/number_7.png -------------------------------------------------------------------------------- /res/drawable-hdpi/number_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/number_8.png -------------------------------------------------------------------------------- /res/drawable-hdpi/number_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/number_9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/play.png -------------------------------------------------------------------------------- /res/drawable-hdpi/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/record.png -------------------------------------------------------------------------------- /res/drawable-hdpi/stat_sys_call_record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/stat_sys_call_record.png -------------------------------------------------------------------------------- /res/drawable-hdpi/stat_sys_call_record_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/stat_sys_call_record_full.png -------------------------------------------------------------------------------- /res/drawable-hdpi/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/stop.png -------------------------------------------------------------------------------- /res/drawable-hdpi/tape_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/tape_bottom.png -------------------------------------------------------------------------------- /res/drawable-hdpi/tape_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/tape_top.png -------------------------------------------------------------------------------- /res/drawable-hdpi/wheel_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/wheel_left.png -------------------------------------------------------------------------------- /res/drawable-hdpi/wheel_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/wheel_right.png -------------------------------------------------------------------------------- /res/drawable-hdpi/wheel_small_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/wheel_small_left.png -------------------------------------------------------------------------------- /res/drawable-hdpi/wheel_small_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiCode/SoundRecorder/16efc01ecd11e3927d1915be0789856f96483881/res/drawable-hdpi/wheel_small_right.png -------------------------------------------------------------------------------- /res/drawable/btn_delete.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 20 | 23 | 26 | 29 | 31 | 32 | -------------------------------------------------------------------------------- /res/drawable/btn_finish.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 20 | 23 | 26 | 29 | 31 | 32 | -------------------------------------------------------------------------------- /res/drawable/btn_new.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 20 | 23 | 26 | 29 | 31 | -------------------------------------------------------------------------------- /res/drawable/btn_pause.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 20 | 23 | 26 | 28 | 29 | -------------------------------------------------------------------------------- /res/drawable/btn_play.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 20 | 23 | 26 | 28 | 29 | -------------------------------------------------------------------------------- /res/drawable/btn_record.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 20 | 23 | 26 | 28 | 29 | -------------------------------------------------------------------------------- /res/drawable/btn_stop.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 20 | 23 | 26 | 28 | 29 | -------------------------------------------------------------------------------- /res/layout/context_menu_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 24 | 25 | 33 | 34 | 42 | 43 | -------------------------------------------------------------------------------- /res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 29 | 30 | 38 | 39 | 40 | 44 | 45 | 49 | 50 | 58 | 59 | 67 | 68 | 76 | 77 | 85 | 86 | 90 | 91 | 98 | 99 | 104 | 105 | 112 | 113 | 114 | 115 | 121 | 122 | 129 | 130 | 131 | 137 | 138 | 145 | 146 | 154 | 155 | 162 | 163 | 164 | 165 | 166 | 171 | 172 | 176 | 177 | 182 | 183 | 189 | 190 | 195 | 196 | 202 | 203 | 209 | 210 | 216 | 217 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /res/layout/view_list_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 20 | 24 | 28 | -------------------------------------------------------------------------------- /res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 20 | "录音机" 21 | "录音" 22 | "录音停止" 23 | "存储空间已满" 24 | "已达到长度上限" 25 | "需要SD卡才可使用" 26 | "确定" 27 | 28 | 29 | "您的录音" 30 | "录音" 31 | "我的录音" 32 | "无法访问存储器" 33 | "内部应用程序错误" 34 | "无法保存录制的音频" 35 | 36 | 未命名录音 37 | 查看录音列表 38 | 设置 39 | 您确定要删除这段录音吗? 40 | %s已存在。\n确定要覆盖这段录音吗? 41 | 设置 42 | 录音文件类型 43 | 请选择录音文件类型 44 | 使用高质量录音 45 | 高质量录音会增大录音文件 46 | 打开音效 47 | 操作录音机时播放音效 48 | 正在录音... 49 | 录音已终止 50 | 存储空间不足(小于%d分钟) 51 | SD卡不存在 52 | 打开文件夹 53 | 确认要删除所选的录音吗? 54 | 55 | -------------------------------------------------------------------------------- /res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 19 | 20 | 0 21 | 0 22 | 5 23 | 120 24 | 8 25 | -5 26 | 5 27 | 0 28 | 5 29 | 30 | 31 | 32 | 0 33 | 0 34 | 5 35 | 5 36 | 10 37 | -3 38 | 10 39 | 15 40 | 10 41 | -3 42 | 10 43 | 25 44 | 10 45 | -3 46 | 10 47 | 35 48 | 10 49 | -3 50 | 10 51 | 40 52 | 10 53 | -3 54 | 30 55 | 120 56 | 8 57 | -5 58 | 5 59 | 0 60 | 150 61 | 50 62 | 8 63 | -5 64 | 5 65 | 0 66 | 5 67 | 68 | 69 | 70 | 0 71 | 0 72 | 5 73 | 5 74 | 50 75 | -3 76 | 5 77 | 10 78 | 50 79 | -3 80 | 5 81 | 5 82 | 50 83 | -3 84 | 5 85 | 10 86 | 50 87 | -3 88 | 5 89 | 5 90 | 50 91 | -3 92 | 5 93 | 10 94 | 50 95 | -3 96 | 5 97 | 5 98 | 50 99 | -3 100 | 5 101 | 10 102 | 50 103 | -3 104 | 5 105 | 5 106 | 50 107 | -3 108 | 5 109 | 10 110 | 50 111 | -3 112 | 5 113 | 5 114 | 50 115 | -3 116 | 5 117 | 10 118 | 50 119 | -3 120 | 5 121 | 5 122 | 50 123 | -3 124 | 5 125 | 10 126 | 50 127 | -3 128 | 5 129 | 120 130 | 8 131 | -5 132 | 5 133 | 0 134 | 150 135 | 60 136 | 8 137 | -5 138 | 5 139 | 0 140 | 5 141 | 0 142 | 10 143 | 144 | 145 | 146 | 0 147 | 0 148 | 5 149 | 120 150 | 8 151 | -5 152 | 5 153 | 0 154 | 150 155 | 60 156 | 8 157 | -5 158 | 5 159 | 0 160 | 5 161 | 162 | -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 19 | 20 | Sound Recorder 21 | 22 | 23 | 24 | 25 | 26 | Recording 27 | 28 | Recording stopped 29 | 30 | Storage is full 31 | 32 | Maximum length reached 33 | 34 | SD card is required 35 | 36 | 37 | 38 | 39 | 40 | 41 | OK 42 | 43 | %02d:%02d 44 | 45 | 46 | 47 | yyyy-MM-dd HH:mm:ss 48 | 49 | Your recordings 50 | 51 | Audio recordings 52 | 53 | My recordings 54 | 55 | Unable to access external or internal storage 56 | 57 | Internal application error 58 | 59 | Unable to save recorded audio 60 | 61 | 62 | untitled record 63 | View record list 64 | Settings 65 | Do you want to delete current record? 66 | %s has already existed. Do you want to overwrite it? 67 | Settings 68 | Record file type 69 | Please select file type 70 | audio/3gpp 71 | Enable high quality 72 | The audio file with high quality can be larger 73 | Enable sound effect 74 | Playing sound effect when the state is changed 75 | Recording... 76 | Recording is stopped 77 | Low storage(less than %d minutes)... 78 | SD card is not available 79 | Open file explorer 80 | Do you want to delete selected record? 81 | 82 | 83 | amr 84 | 3gpp 85 | 86 | 87 | audio/amr 88 | audio/3gpp 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 20 | 21 | 28 | 33 | 34 | 35 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/net/micode/soundrecorder/RecordNameEditText.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.micode.soundrecorder; 18 | 19 | import android.content.Context; 20 | import android.graphics.Rect; 21 | import android.text.TextUtils; 22 | import android.util.AttributeSet; 23 | import android.view.KeyEvent; 24 | import android.view.inputmethod.InputMethodManager; 25 | import android.widget.EditText; 26 | 27 | import java.io.File; 28 | import java.text.SimpleDateFormat; 29 | import java.util.Calendar; 30 | 31 | public class RecordNameEditText extends EditText { 32 | 33 | private Context mContext; 34 | 35 | private InputMethodManager mInputMethodManager; 36 | 37 | private OnNameChangeListener mNameChangeListener; 38 | 39 | private String mDir; 40 | 41 | private String mExtension; 42 | 43 | private String mOriginalName; 44 | 45 | public interface OnNameChangeListener { 46 | 47 | void onNameChanged(String name); 48 | } 49 | 50 | public RecordNameEditText(Context context) { 51 | super(context, null); 52 | mContext = context; 53 | mInputMethodManager = (InputMethodManager) context 54 | .getSystemService(Context.INPUT_METHOD_SERVICE); 55 | mNameChangeListener = null; 56 | } 57 | 58 | public RecordNameEditText(Context context, AttributeSet attrs) { 59 | super(context, attrs); 60 | mContext = context; 61 | mInputMethodManager = (InputMethodManager) context 62 | .getSystemService(Context.INPUT_METHOD_SERVICE); 63 | mNameChangeListener = null; 64 | } 65 | 66 | public RecordNameEditText(Context context, AttributeSet attrs, int defStyle) { 67 | super(context, attrs, defStyle); 68 | mContext = context; 69 | mInputMethodManager = (InputMethodManager) context 70 | .getSystemService(Context.INPUT_METHOD_SERVICE); 71 | mNameChangeListener = null; 72 | } 73 | 74 | public void setNameChangeListener(OnNameChangeListener listener) { 75 | mNameChangeListener = listener; 76 | } 77 | 78 | public void initFileName(String dir, String extension, boolean englishOnly) { 79 | mDir = dir; 80 | mExtension = extension; 81 | 82 | // initialize the default name 83 | if (!englishOnly) { 84 | setText(getProperFileName(mContext.getString(R.string.default_record_name))); 85 | } else { 86 | SimpleDateFormat dataFormat = new SimpleDateFormat("MMddHHmmss"); 87 | setText(getProperFileName("rec_" + dataFormat.format(Calendar.getInstance().getTime()))); 88 | } 89 | } 90 | 91 | private String getProperFileName(String name) { 92 | String uniqueName = name; 93 | 94 | if (isFileExisted(uniqueName)) { 95 | int i = 2; 96 | while (true) { 97 | String temp = uniqueName + "(" + i + ")"; 98 | if (!isFileExisted(temp)) { 99 | uniqueName = temp; 100 | break; 101 | } 102 | i++; 103 | } 104 | } 105 | return uniqueName; 106 | } 107 | 108 | private boolean isFileExisted(String name) { 109 | String fullName = mDir + "/" + name.trim() + mExtension; 110 | File file = new File(fullName); 111 | return file.exists(); 112 | } 113 | 114 | @Override 115 | public boolean onKeyUp(int keyCode, KeyEvent event) { 116 | switch (keyCode) { 117 | case KeyEvent.KEYCODE_ENTER: 118 | if (mNameChangeListener != null) { 119 | String name = getText().toString().trim(); 120 | if (!TextUtils.isEmpty(name)) { 121 | // use new name 122 | setText(name); 123 | mNameChangeListener.onNameChanged(name); 124 | 125 | } else { 126 | // use original name 127 | setText(mOriginalName); 128 | } 129 | clearFocus(); 130 | 131 | // hide the keyboard 132 | mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); 133 | return true; 134 | } 135 | break; 136 | default: 137 | break; 138 | } 139 | return super.onKeyUp(keyCode, event); 140 | } 141 | 142 | @Override 143 | protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 144 | super.onFocusChanged(focused, direction, previouslyFocusedRect); 145 | if (!focused && mNameChangeListener != null) { 146 | String name = getText().toString().trim(); 147 | if (!TextUtils.isEmpty(name)) { 148 | // use new name 149 | setText(name); 150 | mNameChangeListener.onNameChanged(name); 151 | 152 | } else { 153 | // use original name 154 | setText(mOriginalName); 155 | } 156 | 157 | // hide the keyboard 158 | mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); 159 | } else if (focused) { 160 | mOriginalName = getText().toString(); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/net/micode/soundrecorder/Recorder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.micode.soundrecorder; 18 | 19 | import java.io.File; 20 | import java.io.IOException; 21 | 22 | import android.content.Context; 23 | import android.media.MediaPlayer; 24 | import android.media.MediaPlayer.OnCompletionListener; 25 | import android.media.MediaPlayer.OnErrorListener; 26 | import android.os.Bundle; 27 | import android.os.Environment; 28 | import android.text.TextUtils; 29 | 30 | public class Recorder implements OnCompletionListener, OnErrorListener { 31 | private static final String SAMPLE_PREFIX = "recording"; 32 | 33 | private static final String SAMPLE_PATH_KEY = "sample_path"; 34 | 35 | private static final String SAMPLE_LENGTH_KEY = "sample_length"; 36 | 37 | public static final String SAMPLE_DEFAULT_DIR = "/sound_recorder"; 38 | 39 | public static final int IDLE_STATE = 0; 40 | 41 | public static final int RECORDING_STATE = 1; 42 | 43 | public static final int PLAYING_STATE = 2; 44 | 45 | public static final int PLAYING_PAUSED_STATE = 3; 46 | 47 | private int mState = IDLE_STATE; 48 | 49 | public static final int NO_ERROR = 0; 50 | 51 | public static final int STORAGE_ACCESS_ERROR = 1; 52 | 53 | public static final int INTERNAL_ERROR = 2; 54 | 55 | public static final int IN_CALL_RECORD_ERROR = 3; 56 | 57 | public interface OnStateChangedListener { 58 | public void onStateChanged(int state); 59 | 60 | public void onError(int error); 61 | } 62 | 63 | private Context mContext; 64 | 65 | private OnStateChangedListener mOnStateChangedListener = null; 66 | 67 | private long mSampleStart = 0; // time at which latest record or play 68 | // operation started 69 | 70 | private int mSampleLength = 0; // length of current sample 71 | 72 | private File mSampleFile = null; 73 | 74 | private File mSampleDir = null; 75 | 76 | private MediaPlayer mPlayer = null; 77 | 78 | public Recorder(Context context) { 79 | mContext = context; 80 | File sampleDir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() 81 | + SAMPLE_DEFAULT_DIR); 82 | if (!sampleDir.exists()) { 83 | sampleDir.mkdirs(); 84 | } 85 | mSampleDir = sampleDir; 86 | 87 | syncStateWithService(); 88 | } 89 | 90 | public boolean syncStateWithService() { 91 | if (RecorderService.isRecording()) { 92 | mState = RECORDING_STATE; 93 | mSampleStart = RecorderService.getStartTime(); 94 | mSampleFile = new File(RecorderService.getFilePath()); 95 | return true; 96 | } else if (mState == RECORDING_STATE) { 97 | // service is idle but local state is recording 98 | return false; 99 | } else if (mSampleFile != null && mSampleLength == 0) { 100 | // this state can be reached if there is an incoming call 101 | // the record service is stopped by incoming call without notifying 102 | // the UI 103 | return false; 104 | } 105 | return true; 106 | } 107 | 108 | public void saveState(Bundle recorderState) { 109 | recorderState.putString(SAMPLE_PATH_KEY, mSampleFile.getAbsolutePath()); 110 | recorderState.putInt(SAMPLE_LENGTH_KEY, mSampleLength); 111 | } 112 | 113 | public String getRecordDir() { 114 | return mSampleDir.getAbsolutePath(); 115 | } 116 | 117 | public int getMaxAmplitude() { 118 | if (mState != RECORDING_STATE) 119 | return 0; 120 | return RecorderService.getMaxAmplitude(); 121 | } 122 | 123 | public void restoreState(Bundle recorderState) { 124 | String samplePath = recorderState.getString(SAMPLE_PATH_KEY); 125 | if (samplePath == null) 126 | return; 127 | int sampleLength = recorderState.getInt(SAMPLE_LENGTH_KEY, -1); 128 | if (sampleLength == -1) 129 | return; 130 | 131 | File file = new File(samplePath); 132 | if (!file.exists()) 133 | return; 134 | if (mSampleFile != null 135 | && mSampleFile.getAbsolutePath().compareTo(file.getAbsolutePath()) == 0) 136 | return; 137 | 138 | delete(); 139 | mSampleFile = file; 140 | mSampleLength = sampleLength; 141 | 142 | signalStateChanged(IDLE_STATE); 143 | } 144 | 145 | public void setOnStateChangedListener(OnStateChangedListener listener) { 146 | mOnStateChangedListener = listener; 147 | } 148 | 149 | public int state() { 150 | return mState; 151 | } 152 | 153 | public int progress() { 154 | if (mState == RECORDING_STATE) { 155 | return (int) ((System.currentTimeMillis() - mSampleStart) / 1000); 156 | } else if (mState == PLAYING_STATE || mState == PLAYING_PAUSED_STATE) { 157 | if (mPlayer != null) { 158 | return (int) (mPlayer.getCurrentPosition() / 1000); 159 | } 160 | } 161 | 162 | return 0; 163 | } 164 | 165 | public float playProgress() { 166 | if (mPlayer != null) { 167 | return ((float) mPlayer.getCurrentPosition()) / mPlayer.getDuration(); 168 | } 169 | return 0.0f; 170 | } 171 | 172 | public int sampleLength() { 173 | return mSampleLength; 174 | } 175 | 176 | public File sampleFile() { 177 | return mSampleFile; 178 | } 179 | 180 | public void renameSampleFile(String name) { 181 | if (mSampleFile != null && mState != RECORDING_STATE && mState != PLAYING_STATE) { 182 | if (!TextUtils.isEmpty(name)) { 183 | String oldName = mSampleFile.getAbsolutePath(); 184 | String extension = oldName.substring(oldName.lastIndexOf('.')); 185 | File newFile = new File(mSampleFile.getParent() + "/" + name + extension); 186 | if (!TextUtils.equals(oldName, newFile.getAbsolutePath())) { 187 | if (mSampleFile.renameTo(newFile)) { 188 | mSampleFile = newFile; 189 | } 190 | } 191 | } 192 | } 193 | } 194 | 195 | /** 196 | * Resets the recorder state. If a sample was recorded, the file is deleted. 197 | */ 198 | public void delete() { 199 | stop(); 200 | 201 | if (mSampleFile != null) 202 | mSampleFile.delete(); 203 | 204 | mSampleFile = null; 205 | mSampleLength = 0; 206 | 207 | signalStateChanged(IDLE_STATE); 208 | } 209 | 210 | /** 211 | * Resets the recorder state. If a sample was recorded, the file is left on 212 | * disk and will be reused for a new recording. 213 | */ 214 | public void clear() { 215 | stop(); 216 | mSampleLength = 0; 217 | signalStateChanged(IDLE_STATE); 218 | } 219 | 220 | public void reset() { 221 | stop(); 222 | 223 | mSampleLength = 0; 224 | mSampleFile = null; 225 | mState = IDLE_STATE; 226 | 227 | File sampleDir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() 228 | + SAMPLE_DEFAULT_DIR); 229 | if (!sampleDir.exists()) { 230 | sampleDir.mkdirs(); 231 | } 232 | mSampleDir = sampleDir; 233 | 234 | signalStateChanged(IDLE_STATE); 235 | } 236 | 237 | public boolean isRecordExisted(String path) { 238 | if (!TextUtils.isEmpty(path)) { 239 | File file = new File(mSampleDir.getAbsolutePath() + "/" + path); 240 | return file.exists(); 241 | } 242 | return false; 243 | } 244 | 245 | public void startRecording(int outputfileformat, String name, String extension, 246 | boolean highQuality, long maxFileSize) { 247 | stop(); 248 | 249 | if (mSampleFile == null) { 250 | try { 251 | mSampleFile = File.createTempFile(SAMPLE_PREFIX, extension, mSampleDir); 252 | renameSampleFile(name); 253 | } catch (IOException e) { 254 | setError(STORAGE_ACCESS_ERROR); 255 | return; 256 | } 257 | } 258 | 259 | RecorderService.startRecording(mContext, outputfileformat, mSampleFile.getAbsolutePath(), 260 | highQuality, maxFileSize); 261 | mSampleStart = System.currentTimeMillis(); 262 | } 263 | 264 | public void stopRecording() { 265 | if (RecorderService.isRecording()) { 266 | RecorderService.stopRecording(mContext); 267 | mSampleLength = (int) ((System.currentTimeMillis() - mSampleStart) / 1000); 268 | if (mSampleLength == 0) { 269 | // round up to 1 second if it's too short 270 | mSampleLength = 1; 271 | } 272 | } 273 | } 274 | 275 | public void startPlayback(float percentage) { 276 | if (state() == PLAYING_PAUSED_STATE) { 277 | mSampleStart = System.currentTimeMillis() - mPlayer.getCurrentPosition(); 278 | mPlayer.seekTo((int) (percentage * mPlayer.getDuration())); 279 | mPlayer.start(); 280 | setState(PLAYING_STATE); 281 | } else { 282 | stop(); 283 | 284 | mPlayer = new MediaPlayer(); 285 | try { 286 | mPlayer.setDataSource(mSampleFile.getAbsolutePath()); 287 | mPlayer.setOnCompletionListener(this); 288 | mPlayer.setOnErrorListener(this); 289 | mPlayer.prepare(); 290 | mPlayer.seekTo((int) (percentage * mPlayer.getDuration())); 291 | mPlayer.start(); 292 | } catch (IllegalArgumentException e) { 293 | setError(INTERNAL_ERROR); 294 | mPlayer = null; 295 | return; 296 | } catch (IOException e) { 297 | setError(STORAGE_ACCESS_ERROR); 298 | mPlayer = null; 299 | return; 300 | } 301 | 302 | mSampleStart = System.currentTimeMillis(); 303 | setState(PLAYING_STATE); 304 | } 305 | } 306 | 307 | public void pausePlayback() { 308 | if (mPlayer == null) { 309 | return; 310 | } 311 | 312 | mPlayer.pause(); 313 | setState(PLAYING_PAUSED_STATE); 314 | } 315 | 316 | public void stopPlayback() { 317 | if (mPlayer == null) // we were not in playback 318 | return; 319 | 320 | mPlayer.stop(); 321 | mPlayer.release(); 322 | mPlayer = null; 323 | setState(IDLE_STATE); 324 | } 325 | 326 | public void stop() { 327 | stopRecording(); 328 | stopPlayback(); 329 | } 330 | 331 | public boolean onError(MediaPlayer mp, int what, int extra) { 332 | stop(); 333 | setError(STORAGE_ACCESS_ERROR); 334 | return true; 335 | } 336 | 337 | public void onCompletion(MediaPlayer mp) { 338 | stop(); 339 | } 340 | 341 | public void setState(int state) { 342 | if (state == mState) 343 | return; 344 | 345 | mState = state; 346 | signalStateChanged(mState); 347 | } 348 | 349 | private void signalStateChanged(int state) { 350 | if (mOnStateChangedListener != null) 351 | mOnStateChangedListener.onStateChanged(state); 352 | } 353 | 354 | public void setError(int error) { 355 | if (mOnStateChangedListener != null) 356 | mOnStateChangedListener.onError(error); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/net/micode/soundrecorder/RecorderService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.micode.soundrecorder; 18 | 19 | import android.app.KeyguardManager; 20 | import android.app.Notification; 21 | import android.app.NotificationManager; 22 | import android.app.PendingIntent; 23 | import android.app.Service; 24 | import android.content.Context; 25 | import android.content.Intent; 26 | import android.media.AudioManager; 27 | import android.media.MediaRecorder; 28 | import android.net.Uri; 29 | import android.os.Bundle; 30 | import android.os.Handler; 31 | import android.os.IBinder; 32 | import android.os.PowerManager; 33 | import android.os.PowerManager.WakeLock; 34 | import android.telephony.PhoneStateListener; 35 | import android.telephony.TelephonyManager; 36 | 37 | import java.io.File; 38 | import java.io.IOException; 39 | 40 | public class RecorderService extends Service implements MediaRecorder.OnErrorListener { 41 | 42 | public final static String ACTION_NAME = "action_type"; 43 | 44 | public final static int ACTION_INVALID = 0; 45 | 46 | public final static int ACTION_START_RECORDING = 1; 47 | 48 | public final static int ACTION_STOP_RECORDING = 2; 49 | 50 | public final static int ACTION_ENABLE_MONITOR_REMAIN_TIME = 3; 51 | 52 | public final static int ACTION_DISABLE_MONITOR_REMAIN_TIME = 4; 53 | 54 | public final static String ACTION_PARAM_FORMAT = "format"; 55 | 56 | public final static String ACTION_PARAM_PATH = "path"; 57 | 58 | public final static String ACTION_PARAM_HIGH_QUALITY = "high_quality"; 59 | 60 | public final static String ACTION_PARAM_MAX_FILE_SIZE = "max_file_size"; 61 | 62 | public final static String RECORDER_SERVICE_BROADCAST_NAME = "com.android.soundrecorder.broadcast"; 63 | 64 | public final static String RECORDER_SERVICE_BROADCAST_STATE = "is_recording"; 65 | 66 | public final static String RECORDER_SERVICE_BROADCAST_ERROR = "error_code"; 67 | 68 | public final static int NOTIFICATION_ID = 62343234; 69 | 70 | private static MediaRecorder mRecorder = null; 71 | 72 | private static String mFilePath = null; 73 | 74 | private static long mStartTime = 0; 75 | 76 | private RemainingTimeCalculator mRemainingTimeCalculator; 77 | 78 | private NotificationManager mNotifiManager; 79 | 80 | private Notification mLowStorageNotification; 81 | 82 | private TelephonyManager mTeleManager; 83 | 84 | private WakeLock mWakeLock; 85 | 86 | private KeyguardManager mKeyguardManager; 87 | 88 | private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() { 89 | @Override 90 | public void onCallStateChanged(int state, String incomingNumber) { 91 | if (state != TelephonyManager.CALL_STATE_IDLE) { 92 | localStopRecording(); 93 | } 94 | } 95 | }; 96 | 97 | private final Handler mHandler = new Handler(); 98 | 99 | private Runnable mUpdateRemainingTime = new Runnable() { 100 | public void run() { 101 | if (mRecorder != null && mNeedUpdateRemainingTime) { 102 | updateRemainingTime(); 103 | } 104 | } 105 | }; 106 | 107 | private boolean mNeedUpdateRemainingTime; 108 | 109 | @Override 110 | public void onCreate() { 111 | super.onCreate(); 112 | mRecorder = null; 113 | mLowStorageNotification = null; 114 | mRemainingTimeCalculator = new RemainingTimeCalculator(); 115 | mNeedUpdateRemainingTime = false; 116 | mNotifiManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 117 | mTeleManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); 118 | mTeleManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); 119 | PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 120 | mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "SoundRecorder"); 121 | mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); 122 | } 123 | 124 | @Override 125 | public int onStartCommand(Intent intent, int flags, int startId) { 126 | Bundle bundle = intent.getExtras(); 127 | if (bundle != null && bundle.containsKey(ACTION_NAME)) { 128 | switch (bundle.getInt(ACTION_NAME, ACTION_INVALID)) { 129 | case ACTION_START_RECORDING: 130 | localStartRecording(bundle.getInt(ACTION_PARAM_FORMAT), 131 | bundle.getString(ACTION_PARAM_PATH), 132 | bundle.getBoolean(ACTION_PARAM_HIGH_QUALITY), 133 | bundle.getLong(ACTION_PARAM_MAX_FILE_SIZE)); 134 | break; 135 | case ACTION_STOP_RECORDING: 136 | localStopRecording(); 137 | break; 138 | case ACTION_ENABLE_MONITOR_REMAIN_TIME: 139 | if (mRecorder != null) { 140 | mNeedUpdateRemainingTime = true; 141 | mHandler.post(mUpdateRemainingTime); 142 | } 143 | break; 144 | case ACTION_DISABLE_MONITOR_REMAIN_TIME: 145 | mNeedUpdateRemainingTime = false; 146 | if (mRecorder != null) { 147 | showRecordingNotification(); 148 | } 149 | break; 150 | default: 151 | break; 152 | } 153 | return START_STICKY; 154 | } 155 | return super.onStartCommand(intent, flags, startId); 156 | } 157 | 158 | @Override 159 | public void onDestroy() { 160 | mTeleManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); 161 | if (mWakeLock.isHeld()) { 162 | mWakeLock.release(); 163 | } 164 | super.onDestroy(); 165 | } 166 | 167 | @Override 168 | public IBinder onBind(Intent intent) { 169 | return null; 170 | } 171 | 172 | @Override 173 | public void onLowMemory() { 174 | localStopRecording(); 175 | super.onLowMemory(); 176 | } 177 | 178 | private void localStartRecording(int outputfileformat, String path, boolean highQuality, 179 | long maxFileSize) { 180 | if (mRecorder == null) { 181 | mRemainingTimeCalculator.reset(); 182 | if (maxFileSize != -1) { 183 | mRemainingTimeCalculator.setFileSizeLimit(new File(path), maxFileSize); 184 | } 185 | 186 | mRecorder = new MediaRecorder(); 187 | mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 188 | if (outputfileformat == MediaRecorder.OutputFormat.THREE_GPP) { 189 | mRemainingTimeCalculator.setBitRate(SoundRecorder.BITRATE_3GPP); 190 | mRecorder.setAudioSamplingRate(highQuality ? 44100 : 22050); 191 | mRecorder.setOutputFormat(outputfileformat); 192 | mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); 193 | } else { 194 | mRemainingTimeCalculator.setBitRate(SoundRecorder.BITRATE_AMR); 195 | mRecorder.setAudioSamplingRate(highQuality ? 16000 : 8000); 196 | mRecorder.setOutputFormat(outputfileformat); 197 | mRecorder.setAudioEncoder(highQuality ? MediaRecorder.AudioEncoder.AMR_WB 198 | : MediaRecorder.AudioEncoder.AMR_NB); 199 | } 200 | mRecorder.setOutputFile(path); 201 | mRecorder.setOnErrorListener(this); 202 | 203 | // Handle IOException 204 | try { 205 | mRecorder.prepare(); 206 | } catch (IOException exception) { 207 | sendErrorBroadcast(Recorder.INTERNAL_ERROR); 208 | mRecorder.reset(); 209 | mRecorder.release(); 210 | mRecorder = null; 211 | return; 212 | } 213 | // Handle RuntimeException if the recording couldn't start 214 | try { 215 | mRecorder.start(); 216 | } catch (RuntimeException exception) { 217 | AudioManager audioMngr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 218 | boolean isInCall = (audioMngr.getMode() == AudioManager.MODE_IN_CALL); 219 | if (isInCall) { 220 | sendErrorBroadcast(Recorder.IN_CALL_RECORD_ERROR); 221 | } else { 222 | sendErrorBroadcast(Recorder.INTERNAL_ERROR); 223 | } 224 | mRecorder.reset(); 225 | mRecorder.release(); 226 | mRecorder = null; 227 | return; 228 | } 229 | mFilePath = path; 230 | mStartTime = System.currentTimeMillis(); 231 | mWakeLock.acquire(); 232 | mNeedUpdateRemainingTime = false; 233 | sendStateBroadcast(); 234 | showRecordingNotification(); 235 | } 236 | } 237 | 238 | private void localStopRecording() { 239 | if (mRecorder != null) { 240 | mNeedUpdateRemainingTime = false; 241 | try { 242 | mRecorder.stop(); 243 | } catch (RuntimeException e) { 244 | } 245 | mRecorder.release(); 246 | mRecorder = null; 247 | 248 | sendStateBroadcast(); 249 | showStoppedNotification(); 250 | } 251 | stopSelf(); 252 | } 253 | 254 | private void showRecordingNotification() { 255 | Notification notification = new Notification(R.drawable.stat_sys_call_record, 256 | getString(R.string.notification_recording), System.currentTimeMillis()); 257 | notification.flags = Notification.FLAG_ONGOING_EVENT; 258 | PendingIntent pendingIntent; 259 | pendingIntent = PendingIntent 260 | .getActivity(this, 0, new Intent(this, SoundRecorder.class), 0); 261 | 262 | notification.setLatestEventInfo(this, getString(R.string.app_name), 263 | getString(R.string.notification_recording), pendingIntent); 264 | 265 | startForeground(NOTIFICATION_ID, notification); 266 | } 267 | 268 | private void showLowStorageNotification(int minutes) { 269 | if (mKeyguardManager.inKeyguardRestrictedInputMode()) { 270 | // it's not necessary to show this notification in lock-screen 271 | return; 272 | } 273 | 274 | if (mLowStorageNotification == null) { 275 | mLowStorageNotification = new Notification(R.drawable.stat_sys_call_record_full, 276 | getString(R.string.notification_recording), System.currentTimeMillis()); 277 | mLowStorageNotification.flags = Notification.FLAG_ONGOING_EVENT; 278 | } 279 | 280 | PendingIntent pendingIntent; 281 | pendingIntent = PendingIntent 282 | .getActivity(this, 0, new Intent(this, SoundRecorder.class), 0); 283 | 284 | mLowStorageNotification.setLatestEventInfo(this, getString(R.string.app_name), 285 | getString(R.string.notification_warning, minutes), pendingIntent); 286 | startForeground(NOTIFICATION_ID, mLowStorageNotification); 287 | } 288 | 289 | private void showStoppedNotification() { 290 | stopForeground(true); 291 | mLowStorageNotification = null; 292 | 293 | Notification notification = new Notification(R.drawable.stat_sys_call_record, 294 | getString(R.string.notification_stopped), System.currentTimeMillis()); 295 | notification.flags = Notification.FLAG_AUTO_CANCEL; 296 | Intent intent = new Intent(Intent.ACTION_VIEW); 297 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 298 | intent.setType("audio/*"); 299 | intent.setDataAndType(Uri.fromFile(new File(mFilePath)), "audio/*"); 300 | 301 | PendingIntent pendingIntent; 302 | pendingIntent = PendingIntent.getActivity(this, 0, intent, 303 | PendingIntent.FLAG_UPDATE_CURRENT); 304 | 305 | notification.setLatestEventInfo(this, getString(R.string.app_name), 306 | getString(R.string.notification_stopped), pendingIntent); 307 | mNotifiManager.notify(NOTIFICATION_ID, notification); 308 | } 309 | 310 | private void sendStateBroadcast() { 311 | Intent intent = new Intent(RECORDER_SERVICE_BROADCAST_NAME); 312 | intent.putExtra(RECORDER_SERVICE_BROADCAST_STATE, mRecorder != null); 313 | sendBroadcast(intent); 314 | } 315 | 316 | private void sendErrorBroadcast(int error) { 317 | Intent intent = new Intent(RECORDER_SERVICE_BROADCAST_NAME); 318 | intent.putExtra(RECORDER_SERVICE_BROADCAST_ERROR, error); 319 | sendBroadcast(intent); 320 | } 321 | 322 | private void updateRemainingTime() { 323 | long t = mRemainingTimeCalculator.timeRemaining(); 324 | if (t <= 0) { 325 | localStopRecording(); 326 | return; 327 | } else if (t <= 1800 328 | && mRemainingTimeCalculator.currentLowerLimit() != RemainingTimeCalculator.FILE_SIZE_LIMIT) { 329 | // less than half one hour 330 | showLowStorageNotification((int) Math.ceil(t / 60.0)); 331 | } 332 | 333 | if (mRecorder != null && mNeedUpdateRemainingTime) { 334 | mHandler.postDelayed(mUpdateRemainingTime, 500); 335 | } 336 | } 337 | 338 | public static boolean isRecording() { 339 | return mRecorder != null; 340 | } 341 | 342 | public static String getFilePath() { 343 | return mFilePath; 344 | } 345 | 346 | public static long getStartTime() { 347 | return mStartTime; 348 | } 349 | 350 | public static void startRecording(Context context, int outputfileformat, String path, 351 | boolean highQuality, long maxFileSize) { 352 | Intent intent = new Intent(context, RecorderService.class); 353 | intent.putExtra(ACTION_NAME, ACTION_START_RECORDING); 354 | intent.putExtra(ACTION_PARAM_FORMAT, outputfileformat); 355 | intent.putExtra(ACTION_PARAM_PATH, path); 356 | intent.putExtra(ACTION_PARAM_HIGH_QUALITY, highQuality); 357 | intent.putExtra(ACTION_PARAM_MAX_FILE_SIZE, maxFileSize); 358 | context.startService(intent); 359 | } 360 | 361 | public static void stopRecording(Context context) { 362 | Intent intent = new Intent(context, RecorderService.class); 363 | intent.putExtra(ACTION_NAME, ACTION_STOP_RECORDING); 364 | context.startService(intent); 365 | } 366 | 367 | public static int getMaxAmplitude() { 368 | return mRecorder == null ? 0 : mRecorder.getMaxAmplitude(); 369 | } 370 | 371 | @Override 372 | public void onError(MediaRecorder mr, int what, int extra) { 373 | sendErrorBroadcast(Recorder.INTERNAL_ERROR); 374 | localStopRecording(); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/net/micode/soundrecorder/RemainingTimeCalculator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.micode.soundrecorder; 18 | 19 | import android.os.Environment; 20 | import android.os.StatFs; 21 | 22 | import java.io.File; 23 | 24 | /** 25 | * Calculates remaining recording time based on available disk space and 26 | * optionally a maximum recording file size. The reason why this is not trivial 27 | * is that the file grows in blocks every few seconds or so, while we want a 28 | * smooth countdown. 29 | */ 30 | 31 | public class RemainingTimeCalculator { 32 | public static final int UNKNOWN_LIMIT = 0; 33 | 34 | public static final int FILE_SIZE_LIMIT = 1; 35 | 36 | public static final int DISK_SPACE_LIMIT = 2; 37 | 38 | private static final int EXTERNAL_STORAGE_BLOCK_THREADHOLD = 32; 39 | 40 | // which of the two limits we will hit (or have fit) first 41 | private int mCurrentLowerLimit = UNKNOWN_LIMIT; 42 | 43 | // State for tracking file size of recording. 44 | private File mRecordingFile; 45 | 46 | private long mMaxBytes; 47 | 48 | // Rate at which the file grows 49 | private int mBytesPerSecond; 50 | 51 | // time at which number of free blocks last changed 52 | private long mBlocksChangedTime; 53 | 54 | // number of available blocks at that time 55 | private long mLastBlocks; 56 | 57 | // time at which the size of the file has last changed 58 | private long mFileSizeChangedTime; 59 | 60 | // size of the file at that time 61 | private long mLastFileSize; 62 | 63 | public RemainingTimeCalculator() { 64 | } 65 | 66 | /** 67 | * If called, the calculator will return the minimum of two estimates: how 68 | * long until we run out of disk space and how long until the file reaches 69 | * the specified size. 70 | * 71 | * @param file the file to watch 72 | * @param maxBytes the limit 73 | */ 74 | 75 | public void setFileSizeLimit(File file, long maxBytes) { 76 | mRecordingFile = file; 77 | mMaxBytes = maxBytes; 78 | } 79 | 80 | /** 81 | * Resets the interpolation. 82 | */ 83 | public void reset() { 84 | mCurrentLowerLimit = UNKNOWN_LIMIT; 85 | mBlocksChangedTime = -1; 86 | mFileSizeChangedTime = -1; 87 | } 88 | 89 | /** 90 | * Returns how long (in seconds) we can continue recording. 91 | */ 92 | public long timeRemaining() { 93 | // Calculate how long we can record based on free disk space 94 | StatFs fs = null; 95 | long blocks = -1; 96 | long blockSize = -1; 97 | long now = System.currentTimeMillis(); 98 | 99 | fs = new StatFs(Environment.getExternalStorageDirectory().getAbsolutePath()); 100 | blocks = fs.getAvailableBlocks() - EXTERNAL_STORAGE_BLOCK_THREADHOLD; 101 | blockSize = fs.getBlockSize(); 102 | if (blocks < 0) { 103 | blocks = 0; 104 | } 105 | 106 | if (mBlocksChangedTime == -1 || blocks != mLastBlocks) { 107 | mBlocksChangedTime = now; 108 | mLastBlocks = blocks; 109 | } 110 | 111 | /* 112 | * The calculation below always leaves one free block, since free space 113 | * in the block we're currently writing to is not added. This last block 114 | * might get nibbled when we close and flush the file, but we won't run 115 | * out of disk. 116 | */ 117 | 118 | // at mBlocksChangedTime we had this much time 119 | long result = mLastBlocks * blockSize / mBytesPerSecond; 120 | // so now we have this much time 121 | result -= (now - mBlocksChangedTime) / 1000; 122 | 123 | if (mRecordingFile == null) { 124 | mCurrentLowerLimit = DISK_SPACE_LIMIT; 125 | return result; 126 | } 127 | 128 | // If we have a recording file set, we calculate a second estimate 129 | // based on how long it will take us to reach mMaxBytes. 130 | 131 | mRecordingFile = new File(mRecordingFile.getAbsolutePath()); 132 | long fileSize = mRecordingFile.length(); 133 | if (mFileSizeChangedTime == -1 || fileSize != mLastFileSize) { 134 | mFileSizeChangedTime = now; 135 | mLastFileSize = fileSize; 136 | } 137 | 138 | long result2 = (mMaxBytes - fileSize) / mBytesPerSecond; 139 | result2 -= (now - mFileSizeChangedTime) / 1000; 140 | result2 -= 1; // just for safety 141 | 142 | mCurrentLowerLimit = result < result2 ? DISK_SPACE_LIMIT : FILE_SIZE_LIMIT; 143 | 144 | return Math.min(result, result2); 145 | } 146 | 147 | /** 148 | * Indicates which limit we will hit (or have hit) first, by returning one 149 | * of FILE_SIZE_LIMIT or DISK_SPACE_LIMIT or UNKNOWN_LIMIT. We need this to 150 | * display the correct message to the user when we hit one of the limits. 151 | */ 152 | public int currentLowerLimit() { 153 | return mCurrentLowerLimit; 154 | } 155 | 156 | /** 157 | * Is there any point of trying to start recording? 158 | */ 159 | public boolean diskSpaceAvailable() { 160 | StatFs fs = new StatFs(Environment.getExternalStorageDirectory().getAbsolutePath()); 161 | // keep one free block 162 | return fs.getAvailableBlocks() > EXTERNAL_STORAGE_BLOCK_THREADHOLD; 163 | } 164 | 165 | /** 166 | * Sets the bit rate used in the interpolation. 167 | * 168 | * @param bitRate the bit rate to set in bits/sec. 169 | */ 170 | public void setBitRate(int bitRate) { 171 | mBytesPerSecond = bitRate / 8; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/net/micode/soundrecorder/SeamlessAnimation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.micode.soundrecorder; 18 | 19 | import android.view.animation.Animation; 20 | import android.view.animation.Transformation; 21 | 22 | public class SeamlessAnimation extends Animation { 23 | 24 | private float mFromDegrees; 25 | 26 | private float mToDegrees; 27 | 28 | private float mPivotX; 29 | 30 | private float mPivotY; 31 | 32 | private int mPivotXType; 33 | 34 | private float mPivotXValue; 35 | 36 | private int mPivotYType; 37 | 38 | private float mPivotYValue; 39 | 40 | private boolean mCancelled; 41 | 42 | private float mDegree; 43 | 44 | public SeamlessAnimation(float fromDegrees, float toDegrees, int pivotXType, float pivotXValue, 45 | int pivotYType, float pivotYValue) { 46 | mFromDegrees = fromDegrees; 47 | mToDegrees = toDegrees; 48 | mPivotXType = pivotXType; 49 | mPivotXValue = pivotXValue; 50 | mPivotYType = pivotYType; 51 | mPivotYValue = pivotYValue; 52 | mCancelled = false; 53 | mDegree = fromDegrees; 54 | } 55 | 56 | public void initialize(int width, int height, int parentWidth, int parentHeight) { 57 | super.initialize(width, height, parentWidth, parentHeight); 58 | mPivotX = resolveSize(mPivotXType, mPivotXValue, width, parentWidth); 59 | mPivotY = resolveSize(mPivotYType, mPivotYValue, height, parentHeight); 60 | } 61 | 62 | public float getDegree() { 63 | return mDegree; 64 | } 65 | 66 | @Override 67 | public void cancel() { 68 | super.cancel(); 69 | mCancelled = true; 70 | } 71 | 72 | @Override 73 | protected void applyTransformation(float interpolatedTime, Transformation t) { 74 | if (!mCancelled) { 75 | mDegree = mFromDegrees + ((mToDegrees - mFromDegrees) * interpolatedTime); 76 | } 77 | t.getMatrix().setRotate(mDegree, mPivotX, mPivotY); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/net/micode/soundrecorder/SoundRecorder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.micode.soundrecorder; 18 | 19 | import android.app.Activity; 20 | import android.app.AlertDialog; 21 | import android.app.NotificationManager; 22 | import android.content.BroadcastReceiver; 23 | import android.content.ContentResolver; 24 | import android.content.ContentValues; 25 | import android.content.Context; 26 | import android.content.DialogInterface; 27 | import android.content.Intent; 28 | import android.content.IntentFilter; 29 | import android.content.res.Configuration; 30 | import android.content.res.Resources; 31 | import android.database.Cursor; 32 | import android.media.AudioManager; 33 | import android.media.MediaRecorder; 34 | import android.media.SoundPool; 35 | import android.net.Uri; 36 | import android.os.Build; 37 | import android.os.Bundle; 38 | import android.os.Environment; 39 | import android.os.Handler; 40 | import android.provider.MediaStore; 41 | import android.text.TextUtils; 42 | import android.util.Log; 43 | import android.view.KeyEvent; 44 | import android.view.Menu; 45 | import android.view.MenuItem; 46 | import android.view.View; 47 | import android.view.ViewGroup.LayoutParams; 48 | import android.widget.Button; 49 | import android.widget.ImageButton; 50 | import android.widget.ImageView; 51 | import android.widget.LinearLayout; 52 | import android.widget.SeekBar; 53 | import android.widget.TextView; 54 | import android.widget.Toast; 55 | 56 | import java.io.File; 57 | import java.text.SimpleDateFormat; 58 | import java.util.Date; 59 | import java.util.HashSet; 60 | 61 | public class SoundRecorder extends Activity implements Button.OnClickListener, 62 | Recorder.OnStateChangedListener { 63 | private static final String TAG = "SoundRecorder"; 64 | 65 | private static final String RECORDER_STATE_KEY = "recorder_state"; 66 | 67 | private static final String SAMPLE_INTERRUPTED_KEY = "sample_interrupted"; 68 | 69 | private static final String MAX_FILE_SIZE_KEY = "max_file_size"; 70 | 71 | private static final String AUDIO_3GPP = "audio/3gpp"; 72 | 73 | private static final String AUDIO_AMR = "audio/amr"; 74 | 75 | private static final String AUDIO_ANY = "audio/*"; 76 | 77 | private static final String ANY_ANY = "*/*"; 78 | 79 | private static final String FILE_EXTENSION_AMR = ".amr"; 80 | 81 | private static final String FILE_EXTENSION_3GPP = ".3gpp"; 82 | 83 | public static final int BITRATE_AMR = 2 * 1024 * 8; // bits/sec 84 | 85 | public static final int BITRATE_3GPP = 20 * 1024 * 8; // bits/sec 86 | 87 | private static final int SEEK_BAR_MAX = 10000; 88 | 89 | private static final long WHEEL_SPEED_NORMAL = 1800; 90 | 91 | private static final long WHEEL_SPEED_FAST = 300; 92 | 93 | private static final long WHEEL_SPEED_SUPER_FAST = 100; 94 | 95 | private static final long SMALL_WHEEL_SPEED_NORMAL = 900; 96 | 97 | private static final long SMALL_WHEEL_SPEED_FAST = 200; 98 | 99 | private static final long SMALL_WHEEL_SPEED_SUPER_FAST = 200; 100 | 101 | private String mRequestedType = AUDIO_ANY; 102 | 103 | private boolean mCanRequestChanged = false; 104 | 105 | private Recorder mRecorder; 106 | 107 | private RecorderReceiver mReceiver; 108 | 109 | private boolean mSampleInterrupted = false; 110 | 111 | private boolean mShowFinishButton = false; 112 | 113 | private String mErrorUiMessage = null; // Some error messages are displayed 114 | // in the UI, not a dialog. This 115 | // happens when a recording 116 | // is interrupted for some reason. 117 | 118 | private long mMaxFileSize = -1; // can be specified in the intent 119 | 120 | private RemainingTimeCalculator mRemainingTimeCalculator; 121 | 122 | private String mTimerFormat; 123 | 124 | private SoundPool mSoundPool; 125 | 126 | private int mPlaySound; 127 | 128 | private int mPauseSound; 129 | 130 | private HashSet mSavedRecord; 131 | 132 | private long mLastClickTime; 133 | 134 | private int mLastButtonId; 135 | 136 | private final Handler mHandler = new Handler(); 137 | 138 | private Runnable mUpdateTimer = new Runnable() { 139 | public void run() { 140 | if (!mStopUiUpdate) { 141 | updateTimerView(); 142 | } 143 | } 144 | }; 145 | 146 | private Runnable mUpdateSeekBar = new Runnable() { 147 | @Override 148 | public void run() { 149 | if (!mStopUiUpdate) { 150 | updateSeekBar(); 151 | } 152 | } 153 | }; 154 | 155 | private Runnable mUpdateVUMetur = new Runnable() { 156 | @Override 157 | public void run() { 158 | if (!mStopUiUpdate) { 159 | updateVUMeterView(); 160 | } 161 | } 162 | }; 163 | 164 | private ImageButton mNewButton; 165 | 166 | private ImageButton mFinishButton; 167 | 168 | private ImageButton mRecordButton; 169 | 170 | private ImageButton mStopButton; 171 | 172 | private ImageButton mPlayButton; 173 | 174 | private ImageButton mPauseButton; 175 | 176 | private ImageButton mDeleteButton; 177 | 178 | private WheelImageView mWheelLeft; 179 | 180 | private WheelImageView mWheelRight; 181 | 182 | private WheelImageView mSmallWheelLeft; 183 | 184 | private WheelImageView mSmallWheelRight; 185 | 186 | private RecordNameEditText mFileNameEditText; 187 | 188 | private LinearLayout mTimerLayout; 189 | 190 | private LinearLayout mVUMeterLayout; 191 | 192 | private LinearLayout mSeekBarLayout; 193 | 194 | private TextView mStartTime; 195 | 196 | private TextView mTotalTime; 197 | 198 | private SeekBar mPlaySeekBar; 199 | 200 | private BroadcastReceiver mSDCardMountEventReceiver = null; 201 | 202 | private int mPreviousVUMax; 203 | 204 | private boolean mStopUiUpdate; 205 | 206 | @Override 207 | public void onCreate(Bundle icycle) { 208 | super.onCreate(icycle); 209 | initInternalState(getIntent()); 210 | setContentView(R.layout.main); 211 | 212 | mRecorder = new Recorder(this); 213 | mRecorder.setOnStateChangedListener(this); 214 | mReceiver = new RecorderReceiver(); 215 | mRemainingTimeCalculator = new RemainingTimeCalculator(); 216 | mSavedRecord = new HashSet(); 217 | 218 | initResourceRefs(); 219 | 220 | setResult(RESULT_CANCELED); 221 | registerExternalStorageListener(); 222 | if (icycle != null) { 223 | Bundle recorderState = icycle.getBundle(RECORDER_STATE_KEY); 224 | if (recorderState != null) { 225 | mRecorder.restoreState(recorderState); 226 | mSampleInterrupted = recorderState.getBoolean(SAMPLE_INTERRUPTED_KEY, false); 227 | mMaxFileSize = recorderState.getLong(MAX_FILE_SIZE_KEY, -1); 228 | } 229 | } 230 | 231 | setVolumeControlStream(AudioManager.STREAM_MUSIC); 232 | 233 | if (mShowFinishButton) { 234 | // reset state if it is a recording request 235 | mRecorder.reset(); 236 | resetFileNameEditText(); 237 | } 238 | } 239 | 240 | @Override 241 | protected void onNewIntent(Intent intent) { 242 | super.onNewIntent(intent); 243 | 244 | boolean preShowFinishButton = mShowFinishButton; 245 | initInternalState(intent); 246 | 247 | if (mShowFinishButton || preShowFinishButton != mShowFinishButton) { 248 | // reset state if it is a recording request or state is changed 249 | mRecorder.reset(); 250 | resetFileNameEditText(); 251 | } 252 | } 253 | 254 | private void initInternalState(Intent i) { 255 | mRequestedType = AUDIO_ANY; 256 | mShowFinishButton = false; 257 | if (i != null) { 258 | String s = i.getType(); 259 | if (AUDIO_AMR.equals(s) || AUDIO_3GPP.equals(s) || AUDIO_ANY.equals(s) 260 | || ANY_ANY.equals(s)) { 261 | mRequestedType = s; 262 | mShowFinishButton = true; 263 | } else if (s != null) { 264 | // we only support amr and 3gpp formats right now 265 | setResult(RESULT_CANCELED); 266 | finish(); 267 | return; 268 | } 269 | 270 | final String EXTRA_MAX_BYTES = android.provider.MediaStore.Audio.Media.EXTRA_MAX_BYTES; 271 | mMaxFileSize = i.getLongExtra(EXTRA_MAX_BYTES, -1); 272 | } 273 | 274 | if (AUDIO_ANY.equals(mRequestedType)) { 275 | mRequestedType = SoundRecorderPreferenceActivity.getRecordType(this); 276 | } else if (ANY_ANY.equals(mRequestedType)) { 277 | mRequestedType = AUDIO_3GPP; 278 | } 279 | } 280 | 281 | @Override 282 | public void onConfigurationChanged(Configuration newConfig) { 283 | super.onConfigurationChanged(newConfig); 284 | 285 | setContentView(R.layout.main); 286 | initResourceRefs(); 287 | updateUi(false); 288 | } 289 | 290 | @Override 291 | protected void onSaveInstanceState(Bundle outState) { 292 | super.onSaveInstanceState(outState); 293 | 294 | if (mRecorder.sampleLength() == 0) 295 | return; 296 | 297 | Bundle recorderState = new Bundle(); 298 | 299 | if (mRecorder.state() != Recorder.RECORDING_STATE) { 300 | mRecorder.saveState(recorderState); 301 | } 302 | recorderState.putBoolean(SAMPLE_INTERRUPTED_KEY, mSampleInterrupted); 303 | recorderState.putLong(MAX_FILE_SIZE_KEY, mMaxFileSize); 304 | 305 | outState.putBundle(RECORDER_STATE_KEY, recorderState); 306 | } 307 | 308 | /* 309 | * Whenever the UI is re-created (due f.ex. to orientation change) we have 310 | * to reinitialize references to the views. 311 | */ 312 | private void initResourceRefs() { 313 | mNewButton = (ImageButton) findViewById(R.id.newButton); 314 | mFinishButton = (ImageButton) findViewById(R.id.finishButton); 315 | mRecordButton = (ImageButton) findViewById(R.id.recordButton); 316 | mStopButton = (ImageButton) findViewById(R.id.stopButton); 317 | mPlayButton = (ImageButton) findViewById(R.id.playButton); 318 | mPauseButton = (ImageButton) findViewById(R.id.pauseButton); 319 | mDeleteButton = (ImageButton) findViewById(R.id.deleteButton); 320 | mNewButton.setOnClickListener(this); 321 | mFinishButton.setOnClickListener(this); 322 | mRecordButton.setOnClickListener(this); 323 | mStopButton.setOnClickListener(this); 324 | mPlayButton.setOnClickListener(this); 325 | mPauseButton.setOnClickListener(this); 326 | mDeleteButton.setOnClickListener(this); 327 | 328 | mWheelLeft = (WheelImageView) findViewById(R.id.wheel_left); 329 | mWheelRight = (WheelImageView) findViewById(R.id.wheel_right); 330 | mSmallWheelLeft = (WheelImageView) findViewById(R.id.wheel_small_left); 331 | mSmallWheelRight = (WheelImageView) findViewById(R.id.wheel_small_right); 332 | mFileNameEditText = (RecordNameEditText) findViewById(R.id.file_name); 333 | 334 | resetFileNameEditText(); 335 | mFileNameEditText.setNameChangeListener(new RecordNameEditText.OnNameChangeListener() { 336 | @Override 337 | public void onNameChanged(String name) { 338 | if (!TextUtils.isEmpty(name)) { 339 | mRecorder.renameSampleFile(name); 340 | } 341 | } 342 | }); 343 | 344 | mTimerLayout = (LinearLayout) findViewById(R.id.time_calculator); 345 | mVUMeterLayout = (LinearLayout) findViewById(R.id.vumeter_layout); 346 | mSeekBarLayout = (LinearLayout) findViewById(R.id.play_seek_bar_layout); 347 | mStartTime = (TextView) findViewById(R.id.starttime); 348 | mTotalTime = (TextView) findViewById(R.id.totaltime); 349 | mPlaySeekBar = (SeekBar) findViewById(R.id.play_seek_bar); 350 | mPlaySeekBar.setMax(SEEK_BAR_MAX); 351 | mPlaySeekBar.setOnSeekBarChangeListener(mSeekBarChangeListener); 352 | 353 | mTimerFormat = getResources().getString(R.string.timer_format); 354 | 355 | if (mShowFinishButton) { 356 | mNewButton.setVisibility(View.GONE); 357 | mFinishButton.setVisibility(View.VISIBLE); 358 | mNewButton = mFinishButton; // use mNewButon variable for left 359 | // button in the control panel 360 | } 361 | 362 | mSoundPool = new SoundPool(5, AudioManager.STREAM_SYSTEM, 5); 363 | mPlaySound = mSoundPool.load("/system/media/audio/ui/SoundRecorderPlay.ogg", 1); 364 | mPauseSound = mSoundPool.load("/system/media/audio/ui/SoundRecorderPause.ogg", 1); 365 | 366 | mLastClickTime = 0; 367 | mLastButtonId = 0; 368 | } 369 | 370 | private void resetFileNameEditText() { 371 | String extension = ""; 372 | if (AUDIO_AMR.equals(mRequestedType)) { 373 | extension = FILE_EXTENSION_AMR; 374 | } else if (AUDIO_3GPP.equals(mRequestedType)) { 375 | extension = FILE_EXTENSION_3GPP; 376 | } 377 | 378 | // for audio which is used for mms, we can only use english file name 379 | // mShowFinishButon indicates whether this is an audio for mms 380 | mFileNameEditText.initFileName(mRecorder.getRecordDir(), extension, mShowFinishButton); 381 | } 382 | 383 | private void startRecordPlayingAnimation() { 384 | mWheelLeft.startAnimation(WHEEL_SPEED_NORMAL, true); 385 | mWheelRight.startAnimation(WHEEL_SPEED_NORMAL, true); 386 | mSmallWheelLeft.startAnimation(SMALL_WHEEL_SPEED_NORMAL, true); 387 | mSmallWheelRight.startAnimation(SMALL_WHEEL_SPEED_NORMAL, true); 388 | } 389 | 390 | private void stopRecordPlayingAnimation() { 391 | stopAnimation(); 392 | startRecordPlayingDoneAnimation(); 393 | } 394 | 395 | private void startRecordPlayingDoneAnimation() { 396 | mWheelLeft.startAnimation(WHEEL_SPEED_SUPER_FAST, false, 4); 397 | mWheelRight.startAnimation(WHEEL_SPEED_SUPER_FAST, false, 4); 398 | mSmallWheelLeft.startAnimation(SMALL_WHEEL_SPEED_SUPER_FAST, false, 2); 399 | mSmallWheelRight.startAnimation(SMALL_WHEEL_SPEED_SUPER_FAST, false, 2); 400 | } 401 | 402 | private void startForwardAnimation() { 403 | mWheelLeft.startAnimation(WHEEL_SPEED_FAST, true); 404 | mWheelRight.startAnimation(WHEEL_SPEED_FAST, true); 405 | mSmallWheelLeft.startAnimation(SMALL_WHEEL_SPEED_FAST, true); 406 | mSmallWheelRight.startAnimation(SMALL_WHEEL_SPEED_FAST, true); 407 | } 408 | 409 | private void startBackwardAnimation() { 410 | mWheelLeft.startAnimation(WHEEL_SPEED_FAST, false); 411 | mWheelRight.startAnimation(WHEEL_SPEED_FAST, false); 412 | mSmallWheelLeft.startAnimation(SMALL_WHEEL_SPEED_FAST, false); 413 | mSmallWheelRight.startAnimation(SMALL_WHEEL_SPEED_FAST, false); 414 | } 415 | 416 | private void stopAnimation() { 417 | mWheelLeft.stopAnimation(); 418 | mWheelRight.stopAnimation(); 419 | mSmallWheelLeft.stopAnimation(); 420 | mSmallWheelRight.stopAnimation(); 421 | } 422 | 423 | /* 424 | * Make sure we're not recording music playing in the background, ask the 425 | * MediaPlaybackService to pause playback. 426 | */ 427 | private void stopAudioPlayback() { 428 | // Shamelessly copied from MediaPlaybackService.java, which 429 | // should be public, but isn't. 430 | Intent i = new Intent("com.android.music.musicservicecommand"); 431 | i.putExtra("command", "pause"); 432 | 433 | sendBroadcast(i); 434 | } 435 | 436 | /* 437 | * Handle the buttons. 438 | */ 439 | public void onClick(View button) { 440 | if (System.currentTimeMillis() - mLastClickTime < 300) { 441 | // in order to avoid user click bottom too quickly 442 | return; 443 | } 444 | 445 | if (!button.isEnabled()) 446 | return; 447 | 448 | if (button.getId() == mLastButtonId && button.getId() != R.id.newButton) { 449 | // as the recorder state is async with the UI 450 | // we need to avoid launching the duplicated action 451 | return; 452 | } 453 | 454 | if (button.getId() == R.id.stopButton && System.currentTimeMillis() - mLastClickTime < 1500) { 455 | // it seems that the media recorder is not robust enough 456 | // sometime it crashes when stop recording right after starting 457 | return; 458 | } 459 | 460 | mLastClickTime = System.currentTimeMillis(); 461 | mLastButtonId = button.getId(); 462 | 463 | switch (button.getId()) { 464 | case R.id.newButton: 465 | mFileNameEditText.clearFocus(); 466 | saveSample(); 467 | mRecorder.reset(); 468 | resetFileNameEditText(); 469 | break; 470 | case R.id.recordButton: 471 | showOverwriteConfirmDialogIfConflicts(); 472 | break; 473 | case R.id.stopButton: 474 | mRecorder.stop(); 475 | break; 476 | case R.id.playButton: 477 | mRecorder.startPlayback(mRecorder.playProgress()); 478 | break; 479 | case R.id.pauseButton: 480 | mRecorder.pausePlayback(); 481 | break; 482 | case R.id.finishButton: 483 | mRecorder.stop(); 484 | saveSample(); 485 | finish(); 486 | break; 487 | case R.id.deleteButton: 488 | showDeleteConfirmDialog(); 489 | break; 490 | } 491 | } 492 | 493 | private void startRecording() { 494 | mRemainingTimeCalculator.reset(); 495 | if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 496 | mSampleInterrupted = true; 497 | mErrorUiMessage = getResources().getString(R.string.insert_sd_card); 498 | updateUi(false); 499 | } else if (!mRemainingTimeCalculator.diskSpaceAvailable()) { 500 | mSampleInterrupted = true; 501 | mErrorUiMessage = getResources().getString(R.string.storage_is_full); 502 | updateUi(false); 503 | } else { 504 | stopAudioPlayback(); 505 | 506 | boolean isHighQuality = SoundRecorderPreferenceActivity.isHighQuality(this); 507 | if (AUDIO_AMR.equals(mRequestedType)) { 508 | mRemainingTimeCalculator.setBitRate(BITRATE_AMR); 509 | int outputfileformat = isHighQuality ? MediaRecorder.OutputFormat.AMR_WB 510 | : MediaRecorder.OutputFormat.AMR_NB; 511 | mRecorder.startRecording(outputfileformat, mFileNameEditText.getText().toString(), 512 | FILE_EXTENSION_AMR, isHighQuality, mMaxFileSize); 513 | } else if (AUDIO_3GPP.equals(mRequestedType)) { 514 | // HACKME: for HD2, there is an issue with high quality 3gpp 515 | // use low quality instead 516 | if (Build.MODEL.equals("HTC HD2")) { 517 | isHighQuality = false; 518 | } 519 | 520 | mRemainingTimeCalculator.setBitRate(BITRATE_3GPP); 521 | mRecorder.startRecording(MediaRecorder.OutputFormat.THREE_GPP, mFileNameEditText 522 | .getText().toString(), FILE_EXTENSION_3GPP, isHighQuality, mMaxFileSize); 523 | } else { 524 | throw new IllegalArgumentException("Invalid output file type requested"); 525 | } 526 | 527 | if (mMaxFileSize != -1) { 528 | mRemainingTimeCalculator.setFileSizeLimit(mRecorder.sampleFile(), mMaxFileSize); 529 | } 530 | } 531 | } 532 | 533 | /* 534 | * Handle the "back" hardware key. 535 | */ 536 | @Override 537 | public boolean onKeyDown(int keyCode, KeyEvent event) { 538 | if (keyCode == KeyEvent.KEYCODE_BACK) { 539 | switch (mRecorder.state()) { 540 | case Recorder.IDLE_STATE: 541 | case Recorder.PLAYING_PAUSED_STATE: 542 | if (mRecorder.sampleLength() > 0) 543 | saveSample(); 544 | finish(); 545 | break; 546 | case Recorder.PLAYING_STATE: 547 | mRecorder.stop(); 548 | saveSample(); 549 | break; 550 | case Recorder.RECORDING_STATE: 551 | if (mShowFinishButton) { 552 | mRecorder.clear(); 553 | } else { 554 | finish(); 555 | } 556 | break; 557 | } 558 | return true; 559 | } else { 560 | return super.onKeyDown(keyCode, event); 561 | } 562 | } 563 | 564 | @Override 565 | protected void onResume() { 566 | super.onResume(); 567 | String type = SoundRecorderPreferenceActivity.getRecordType(this); 568 | if (mCanRequestChanged && !TextUtils.equals(type, mRequestedType)) { 569 | saveSample(); 570 | mRecorder.reset(); 571 | mRequestedType = type; 572 | resetFileNameEditText(); 573 | } 574 | mCanRequestChanged = false; 575 | 576 | if (!mRecorder.syncStateWithService()) { 577 | mRecorder.reset(); 578 | resetFileNameEditText(); 579 | } 580 | 581 | if (mRecorder.state() == Recorder.RECORDING_STATE) { 582 | String preExtension = AUDIO_AMR.equals(mRequestedType) ? FILE_EXTENSION_AMR 583 | : FILE_EXTENSION_3GPP; 584 | if (!mRecorder.sampleFile().getName().endsWith(preExtension)) { 585 | // the extension is changed need to stop current recording 586 | mRecorder.reset(); 587 | resetFileNameEditText(); 588 | } else { 589 | // restore state 590 | if (!mShowFinishButton) { 591 | String fileName = mRecorder.sampleFile().getName().replace(preExtension, ""); 592 | mFileNameEditText.setText(fileName); 593 | } 594 | 595 | if (AUDIO_AMR.equals(mRequestedType)) { 596 | mRemainingTimeCalculator.setBitRate(BITRATE_AMR); 597 | } else if (AUDIO_3GPP.equals(mRequestedType)) { 598 | mRemainingTimeCalculator.setBitRate(BITRATE_3GPP); 599 | } 600 | } 601 | } else { 602 | File file = mRecorder.sampleFile(); 603 | if (file != null && !file.exists()) { 604 | mRecorder.reset(); 605 | resetFileNameEditText(); 606 | } 607 | } 608 | 609 | IntentFilter filter = new IntentFilter(); 610 | filter.addAction(RecorderService.RECORDER_SERVICE_BROADCAST_NAME); 611 | registerReceiver(mReceiver, filter); 612 | 613 | mStopUiUpdate = false; 614 | updateUi(true); 615 | 616 | if (RecorderService.isRecording()) { 617 | Intent intent = new Intent(this, RecorderService.class); 618 | intent.putExtra(RecorderService.ACTION_NAME, 619 | RecorderService.ACTION_DISABLE_MONITOR_REMAIN_TIME); 620 | startService(intent); 621 | } 622 | } 623 | 624 | @Override 625 | protected void onPause() { 626 | if (mRecorder.state() != Recorder.RECORDING_STATE || mShowFinishButton 627 | || mMaxFileSize != -1) { 628 | mRecorder.stop(); 629 | saveSample(); 630 | mFileNameEditText.clearFocus(); 631 | ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)) 632 | .cancel(RecorderService.NOTIFICATION_ID); 633 | } 634 | 635 | if (mReceiver != null) { 636 | unregisterReceiver(mReceiver); 637 | } 638 | 639 | mCanRequestChanged = true; 640 | mStopUiUpdate = true; 641 | stopAnimation(); 642 | 643 | if (RecorderService.isRecording()) { 644 | Intent intent = new Intent(this, RecorderService.class); 645 | intent.putExtra(RecorderService.ACTION_NAME, 646 | RecorderService.ACTION_ENABLE_MONITOR_REMAIN_TIME); 647 | startService(intent); 648 | } 649 | 650 | super.onPause(); 651 | } 652 | 653 | @Override 654 | protected void onStop() { 655 | if (mShowFinishButton) { 656 | finish(); 657 | } 658 | super.onStop(); 659 | } 660 | 661 | /* 662 | * If we have just recorded a sample, this adds it to the media data base 663 | * and sets the result to the sample's URI. 664 | */ 665 | private void saveSample() { 666 | if (mRecorder.sampleLength() == 0) 667 | return; 668 | if (!mSavedRecord.contains(mRecorder.sampleFile().getAbsolutePath())) { 669 | Uri uri = null; 670 | try { 671 | uri = this.addToMediaDB(mRecorder.sampleFile()); 672 | } catch (UnsupportedOperationException ex) { // Database 673 | // manipulation 674 | // failure 675 | return; 676 | } 677 | if (uri == null) { 678 | return; 679 | } 680 | mSavedRecord.add(mRecorder.sampleFile().getAbsolutePath()); 681 | setResult(RESULT_OK, new Intent().setData(uri)); 682 | } 683 | } 684 | 685 | private void showDeleteConfirmDialog() { 686 | AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); 687 | dialogBuilder.setIcon(android.R.drawable.ic_dialog_alert); 688 | dialogBuilder.setTitle(R.string.delete_dialog_title); 689 | dialogBuilder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 690 | @Override 691 | public void onClick(DialogInterface dialog, int which) { 692 | mRecorder.delete(); 693 | } 694 | }); 695 | dialogBuilder.setNegativeButton(android.R.string.cancel, 696 | new DialogInterface.OnClickListener() { 697 | @Override 698 | public void onClick(DialogInterface dialog, int which) { 699 | mLastButtonId = 0; 700 | } 701 | }); 702 | dialogBuilder.show(); 703 | } 704 | 705 | private void showOverwriteConfirmDialogIfConflicts() { 706 | String fileName = mFileNameEditText.getText().toString() 707 | + (AUDIO_AMR.equals(mRequestedType) ? FILE_EXTENSION_AMR : FILE_EXTENSION_3GPP); 708 | 709 | if (mRecorder.isRecordExisted(fileName) && !mShowFinishButton) { 710 | // file already existed and it's not a recording request from other 711 | // app 712 | AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); 713 | dialogBuilder.setIcon(android.R.drawable.ic_dialog_alert); 714 | dialogBuilder.setTitle(getString(R.string.overwrite_dialog_title, fileName)); 715 | dialogBuilder.setPositiveButton(android.R.string.ok, 716 | new DialogInterface.OnClickListener() { 717 | @Override 718 | public void onClick(DialogInterface dialog, int which) { 719 | startRecording(); 720 | } 721 | }); 722 | dialogBuilder.setNegativeButton(android.R.string.cancel, 723 | new DialogInterface.OnClickListener() { 724 | @Override 725 | public void onClick(DialogInterface dialog, int which) { 726 | mLastButtonId = 0; 727 | } 728 | }); 729 | dialogBuilder.show(); 730 | } else { 731 | startRecording(); 732 | } 733 | } 734 | 735 | /* 736 | * Called on destroy to unregister the SD card mount event receiver. 737 | */ 738 | @Override 739 | public void onDestroy() { 740 | if (mSDCardMountEventReceiver != null) { 741 | unregisterReceiver(mSDCardMountEventReceiver); 742 | mSDCardMountEventReceiver = null; 743 | } 744 | mSoundPool.release(); 745 | 746 | super.onDestroy(); 747 | } 748 | 749 | /* 750 | * Registers an intent to listen for 751 | * ACTION_MEDIA_EJECT/ACTION_MEDIA_UNMOUNTED/ACTION_MEDIA_MOUNTED 752 | * notifications. 753 | */ 754 | private void registerExternalStorageListener() { 755 | if (mSDCardMountEventReceiver == null) { 756 | mSDCardMountEventReceiver = new BroadcastReceiver() { 757 | @Override 758 | public void onReceive(Context context, Intent intent) { 759 | mSampleInterrupted = false; 760 | mRecorder.reset(); 761 | resetFileNameEditText(); 762 | updateUi(false); 763 | } 764 | }; 765 | IntentFilter iFilter = new IntentFilter(); 766 | iFilter.addAction(Intent.ACTION_MEDIA_EJECT); 767 | iFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); 768 | iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); 769 | iFilter.addDataScheme("file"); 770 | registerReceiver(mSDCardMountEventReceiver, iFilter); 771 | } 772 | } 773 | 774 | /* 775 | * A simple utility to do a query into the databases. 776 | */ 777 | private Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 778 | String sortOrder) { 779 | try { 780 | ContentResolver resolver = getContentResolver(); 781 | if (resolver == null) { 782 | return null; 783 | } 784 | return resolver.query(uri, projection, selection, selectionArgs, sortOrder); 785 | } catch (UnsupportedOperationException ex) { 786 | return null; 787 | } 788 | } 789 | 790 | /* 791 | * Add the given audioId to the playlist with the given playlistId; and 792 | * maintain the play_order in the playlist. 793 | */ 794 | private void addToPlaylist(ContentResolver resolver, int audioId, long playlistId) { 795 | String[] cols = new String[] { 796 | "count(*)" 797 | }; 798 | Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId); 799 | Cursor cur = resolver.query(uri, cols, null, null, null); 800 | cur.moveToFirst(); 801 | final int base = cur.getInt(0); 802 | cur.close(); 803 | ContentValues values = new ContentValues(); 804 | values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + audioId)); 805 | values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId); 806 | resolver.insert(uri, values); 807 | } 808 | 809 | /* 810 | * Obtain the id for the default play list from the audio_playlists table. 811 | */ 812 | private int getPlaylistId(Resources res) { 813 | Uri uri = MediaStore.Audio.Playlists.getContentUri("external"); 814 | final String[] ids = new String[] { 815 | MediaStore.Audio.Playlists._ID 816 | }; 817 | final String where = MediaStore.Audio.Playlists.NAME + "=?"; 818 | final String[] args = new String[] { 819 | res.getString(R.string.audio_db_playlist_name) 820 | }; 821 | Cursor cursor = query(uri, ids, where, args, null); 822 | if (cursor == null) { 823 | Log.v(TAG, "query returns null"); 824 | } 825 | int id = -1; 826 | if (cursor != null) { 827 | cursor.moveToFirst(); 828 | if (!cursor.isAfterLast()) { 829 | id = cursor.getInt(0); 830 | } 831 | cursor.close(); 832 | } 833 | return id; 834 | } 835 | 836 | /* 837 | * Create a playlist with the given default playlist name, if no such 838 | * playlist exists. 839 | */ 840 | private Uri createPlaylist(Resources res, ContentResolver resolver) { 841 | ContentValues cv = new ContentValues(); 842 | cv.put(MediaStore.Audio.Playlists.NAME, res.getString(R.string.audio_db_playlist_name)); 843 | Uri uri = resolver.insert(MediaStore.Audio.Playlists.getContentUri("external"), cv); 844 | if (uri == null) { 845 | new AlertDialog.Builder(this).setTitle(R.string.app_name) 846 | .setMessage(R.string.error_mediadb_new_record) 847 | .setPositiveButton(R.string.button_ok, null).setCancelable(false).show(); 848 | } 849 | return uri; 850 | } 851 | 852 | /* 853 | * Adds file and returns content uri. 854 | */ 855 | private Uri addToMediaDB(File file) { 856 | Resources res = getResources(); 857 | ContentValues cv = new ContentValues(); 858 | long current = System.currentTimeMillis(); 859 | long modDate = file.lastModified(); 860 | Date date = new Date(current); 861 | SimpleDateFormat formatter = new SimpleDateFormat( 862 | res.getString(R.string.audio_db_title_format)); 863 | String title = formatter.format(date); 864 | long sampleLengthMillis = mRecorder.sampleLength() * 1000L; 865 | 866 | // Lets label the recorded audio file as NON-MUSIC so that the file 867 | // won't be displayed automatically, except for in the playlist. 868 | cv.put(MediaStore.Audio.Media.IS_MUSIC, "0"); 869 | 870 | cv.put(MediaStore.Audio.Media.TITLE, title); 871 | cv.put(MediaStore.Audio.Media.DATA, file.getAbsolutePath()); 872 | cv.put(MediaStore.Audio.Media.DATE_ADDED, (int) (current / 1000)); 873 | cv.put(MediaStore.Audio.Media.DATE_MODIFIED, (int) (modDate / 1000)); 874 | cv.put(MediaStore.Audio.Media.DURATION, sampleLengthMillis); 875 | cv.put(MediaStore.Audio.Media.MIME_TYPE, mRequestedType); 876 | cv.put(MediaStore.Audio.Media.ARTIST, res.getString(R.string.audio_db_artist_name)); 877 | cv.put(MediaStore.Audio.Media.ALBUM, res.getString(R.string.audio_db_album_name)); 878 | Log.d(TAG, "Inserting audio record: " + cv.toString()); 879 | ContentResolver resolver = getContentResolver(); 880 | Uri base = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 881 | Log.d(TAG, "ContentURI: " + base); 882 | Uri result = resolver.insert(base, cv); 883 | if (result == null) { 884 | Log.w(TAG, getString(R.string.error_mediadb_new_record)); 885 | return null; 886 | } 887 | 888 | if (getPlaylistId(res) == -1) { 889 | createPlaylist(res, resolver); 890 | } 891 | int audioId = Integer.valueOf(result.getLastPathSegment()); 892 | addToPlaylist(resolver, audioId, getPlaylistId(res)); 893 | 894 | // Notify those applications such as Music listening to the 895 | // scanner events that a recorded audio file just created. 896 | sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result)); 897 | return result; 898 | } 899 | 900 | private ImageView getTimerImage(char number) { 901 | ImageView image = new ImageView(this); 902 | LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 903 | if (number != ':') { 904 | image.setBackgroundResource(R.drawable.background_number); 905 | } 906 | switch (number) { 907 | case '0': 908 | image.setImageResource(R.drawable.number_0); 909 | break; 910 | case '1': 911 | image.setImageResource(R.drawable.number_1); 912 | break; 913 | case '2': 914 | image.setImageResource(R.drawable.number_2); 915 | break; 916 | case '3': 917 | image.setImageResource(R.drawable.number_3); 918 | break; 919 | case '4': 920 | image.setImageResource(R.drawable.number_4); 921 | break; 922 | case '5': 923 | image.setImageResource(R.drawable.number_5); 924 | break; 925 | case '6': 926 | image.setImageResource(R.drawable.number_6); 927 | break; 928 | case '7': 929 | image.setImageResource(R.drawable.number_7); 930 | break; 931 | case '8': 932 | image.setImageResource(R.drawable.number_8); 933 | break; 934 | case '9': 935 | image.setImageResource(R.drawable.number_9); 936 | break; 937 | case ':': 938 | image.setImageResource(R.drawable.colon); 939 | break; 940 | } 941 | image.setLayoutParams(lp); 942 | return image; 943 | } 944 | 945 | /** 946 | * Update the big MM:SS timer. If we are in playback, also update the 947 | * progress bar. 948 | */ 949 | private void updateTimerView() { 950 | int state = mRecorder.state(); 951 | 952 | boolean ongoing = state == Recorder.RECORDING_STATE || state == Recorder.PLAYING_STATE; 953 | 954 | long time = mRecorder.progress(); 955 | String timeStr = String.format(mTimerFormat, time / 60, time % 60); 956 | mTimerLayout.removeAllViews(); 957 | for (int i = 0; i < timeStr.length(); i++) { 958 | mTimerLayout.addView(getTimerImage(timeStr.charAt(i))); 959 | } 960 | 961 | if (state == Recorder.RECORDING_STATE) { 962 | updateTimeRemaining(); 963 | } 964 | 965 | if (ongoing) { 966 | mHandler.postDelayed(mUpdateTimer, 500); 967 | } 968 | } 969 | 970 | private void setTimerView(float progress) { 971 | long time = (long) (progress * mRecorder.sampleLength()); 972 | String timeStr = String.format(mTimerFormat, time / 60, time % 60); 973 | mTimerLayout.removeAllViews(); 974 | for (int i = 0; i < timeStr.length(); i++) { 975 | mTimerLayout.addView(getTimerImage(timeStr.charAt(i))); 976 | } 977 | } 978 | 979 | private void updateSeekBar() { 980 | if (mRecorder.state() == Recorder.PLAYING_STATE) { 981 | mPlaySeekBar.setProgress((int) (SEEK_BAR_MAX * mRecorder.playProgress())); 982 | mHandler.postDelayed(mUpdateSeekBar, 10); 983 | } 984 | } 985 | 986 | /* 987 | * Called when we're in recording state. Find out how much longer we can go 988 | * on recording. If it's under 5 minutes, we display a count-down in the UI. 989 | * If we've run out of time, stop the recording. 990 | */ 991 | private void updateTimeRemaining() { 992 | long t = mRemainingTimeCalculator.timeRemaining(); 993 | 994 | if (t <= 0) { 995 | mSampleInterrupted = true; 996 | 997 | int limit = mRemainingTimeCalculator.currentLowerLimit(); 998 | switch (limit) { 999 | case RemainingTimeCalculator.DISK_SPACE_LIMIT: 1000 | mErrorUiMessage = getResources().getString(R.string.storage_is_full); 1001 | break; 1002 | case RemainingTimeCalculator.FILE_SIZE_LIMIT: 1003 | mErrorUiMessage = getResources().getString(R.string.max_length_reached); 1004 | break; 1005 | default: 1006 | mErrorUiMessage = null; 1007 | break; 1008 | } 1009 | 1010 | mRecorder.stop(); 1011 | return; 1012 | } 1013 | } 1014 | 1015 | private void updateVUMeterView() { 1016 | final int MAX_VU_SIZE = 11; 1017 | boolean showVUArray[] = new boolean[MAX_VU_SIZE]; 1018 | 1019 | if (mVUMeterLayout.getVisibility() == View.VISIBLE 1020 | && mRecorder.state() == Recorder.RECORDING_STATE) { 1021 | int vuSize = MAX_VU_SIZE * mRecorder.getMaxAmplitude() / 32768; 1022 | if (vuSize >= MAX_VU_SIZE) { 1023 | vuSize = MAX_VU_SIZE - 1; 1024 | } 1025 | 1026 | if (vuSize >= mPreviousVUMax) { 1027 | mPreviousVUMax = vuSize; 1028 | } else if (mPreviousVUMax > 0) { 1029 | mPreviousVUMax--; 1030 | } 1031 | 1032 | for (int i = 0; i < MAX_VU_SIZE; i++) { 1033 | if (i <= vuSize) { 1034 | showVUArray[i] = true; 1035 | } else if (i == mPreviousVUMax) { 1036 | showVUArray[i] = true; 1037 | } else { 1038 | showVUArray[i] = false; 1039 | } 1040 | } 1041 | 1042 | mHandler.postDelayed(mUpdateVUMetur, 100); 1043 | } else if (mVUMeterLayout.getVisibility() == View.VISIBLE) { 1044 | mPreviousVUMax = 0; 1045 | for (int i = 0; i < MAX_VU_SIZE; i++) { 1046 | showVUArray[i] = false; 1047 | } 1048 | } 1049 | 1050 | if (mVUMeterLayout.getVisibility() == View.VISIBLE) { 1051 | mVUMeterLayout.removeAllViews(); 1052 | for (boolean show : showVUArray) { 1053 | ImageView imageView = new ImageView(this); 1054 | imageView.setBackgroundResource(R.drawable.background_vumeter); 1055 | if (show) { 1056 | imageView.setImageResource(R.drawable.icon_vumeter); 1057 | } 1058 | imageView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, 1059 | LayoutParams.WRAP_CONTENT)); 1060 | mVUMeterLayout.addView(imageView); 1061 | } 1062 | } 1063 | } 1064 | 1065 | /** 1066 | * Shows/hides the appropriate child views for the new state. 1067 | */ 1068 | private void updateUi(boolean skipRewindAnimation) { 1069 | switch (mRecorder.state()) { 1070 | case Recorder.IDLE_STATE: 1071 | mLastButtonId = 0; 1072 | case Recorder.PLAYING_PAUSED_STATE: 1073 | if (mRecorder.sampleLength() == 0) { 1074 | mNewButton.setEnabled(true); 1075 | mNewButton.setVisibility(View.VISIBLE); 1076 | mRecordButton.setVisibility(View.VISIBLE); 1077 | mStopButton.setVisibility(View.GONE); 1078 | mPlayButton.setVisibility(View.GONE); 1079 | mPauseButton.setVisibility(View.GONE); 1080 | mDeleteButton.setEnabled(false); 1081 | mRecordButton.requestFocus(); 1082 | 1083 | mVUMeterLayout.setVisibility(View.VISIBLE); 1084 | mSeekBarLayout.setVisibility(View.GONE); 1085 | } else { 1086 | mNewButton.setEnabled(true); 1087 | mNewButton.setVisibility(View.VISIBLE); 1088 | mRecordButton.setVisibility(View.GONE); 1089 | mStopButton.setVisibility(View.GONE); 1090 | mPlayButton.setVisibility(View.VISIBLE); 1091 | mPauseButton.setVisibility(View.GONE); 1092 | mDeleteButton.setEnabled(true); 1093 | mPauseButton.requestFocus(); 1094 | 1095 | mVUMeterLayout.setVisibility(View.GONE); 1096 | mSeekBarLayout.setVisibility(View.VISIBLE); 1097 | mStartTime.setText(String.format(mTimerFormat, 0, 0)); 1098 | mTotalTime.setText(String.format(mTimerFormat, mRecorder.sampleLength() / 60, 1099 | mRecorder.sampleLength() % 60)); 1100 | } 1101 | mFileNameEditText.setEnabled(true); 1102 | mFileNameEditText.clearFocus(); 1103 | 1104 | if (mRecorder.sampleLength() > 0) { 1105 | if (mRecorder.state() == Recorder.PLAYING_PAUSED_STATE) { 1106 | stopAnimation(); 1107 | if (SoundRecorderPreferenceActivity.isEnabledSoundEffect(this)) { 1108 | mSoundPool.play(mPauseSound, 1.0f, 1.0f, 0, 0, 1); 1109 | } 1110 | } else { 1111 | mPlaySeekBar.setProgress(0); 1112 | if (!skipRewindAnimation) { 1113 | stopRecordPlayingAnimation(); 1114 | } else { 1115 | stopAnimation(); 1116 | } 1117 | } 1118 | } else { 1119 | stopAnimation(); 1120 | } 1121 | 1122 | // we allow only one toast at one time 1123 | if (mSampleInterrupted && mErrorUiMessage == null) { 1124 | Toast.makeText(this, R.string.recording_stopped, Toast.LENGTH_SHORT).show(); 1125 | } 1126 | 1127 | if (mErrorUiMessage != null) { 1128 | Toast.makeText(this, mErrorUiMessage, Toast.LENGTH_SHORT).show(); 1129 | } 1130 | 1131 | break; 1132 | case Recorder.RECORDING_STATE: 1133 | mNewButton.setEnabled(false); 1134 | mNewButton.setVisibility(View.VISIBLE); 1135 | mRecordButton.setVisibility(View.GONE); 1136 | mStopButton.setVisibility(View.VISIBLE); 1137 | mPlayButton.setVisibility(View.GONE); 1138 | mPauseButton.setVisibility(View.GONE); 1139 | mDeleteButton.setEnabled(false); 1140 | mStopButton.requestFocus(); 1141 | 1142 | mVUMeterLayout.setVisibility(View.VISIBLE); 1143 | mSeekBarLayout.setVisibility(View.GONE); 1144 | 1145 | mFileNameEditText.setEnabled(false); 1146 | 1147 | startRecordPlayingAnimation(); 1148 | mPreviousVUMax = 0; 1149 | break; 1150 | 1151 | case Recorder.PLAYING_STATE: 1152 | mNewButton.setEnabled(false); 1153 | mNewButton.setVisibility(View.VISIBLE); 1154 | mRecordButton.setVisibility(View.GONE); 1155 | mStopButton.setVisibility(View.GONE); 1156 | mPlayButton.setVisibility(View.GONE); 1157 | mPauseButton.setVisibility(View.VISIBLE); 1158 | mDeleteButton.setEnabled(false); 1159 | mPauseButton.requestFocus(); 1160 | 1161 | mVUMeterLayout.setVisibility(View.GONE); 1162 | mSeekBarLayout.setVisibility(View.VISIBLE); 1163 | 1164 | mFileNameEditText.setEnabled(false); 1165 | 1166 | if (SoundRecorderPreferenceActivity.isEnabledSoundEffect(this)) { 1167 | mSoundPool.play(mPlaySound, 1.0f, 1.0f, 0, 0, 1); 1168 | } 1169 | startRecordPlayingAnimation(); 1170 | break; 1171 | } 1172 | 1173 | updateTimerView(); 1174 | updateSeekBar(); 1175 | updateVUMeterView(); 1176 | 1177 | } 1178 | 1179 | /* 1180 | * Called when Recorder changed it's state. 1181 | */ 1182 | public void onStateChanged(int state) { 1183 | if (state == Recorder.PLAYING_STATE || state == Recorder.RECORDING_STATE) { 1184 | mSampleInterrupted = false; 1185 | mErrorUiMessage = null; 1186 | } 1187 | 1188 | updateUi(false); 1189 | } 1190 | 1191 | /* 1192 | * Called when MediaPlayer encounters an error. 1193 | */ 1194 | public void onError(int error) { 1195 | Resources res = getResources(); 1196 | 1197 | String message = null; 1198 | switch (error) { 1199 | case Recorder.STORAGE_ACCESS_ERROR: 1200 | message = res.getString(R.string.error_sdcard_access); 1201 | break; 1202 | case Recorder.IN_CALL_RECORD_ERROR: 1203 | // TODO: update error message to reflect that the recording 1204 | // could not be 1205 | // performed during a call. 1206 | case Recorder.INTERNAL_ERROR: 1207 | message = res.getString(R.string.error_app_internal); 1208 | break; 1209 | } 1210 | if (message != null) { 1211 | new AlertDialog.Builder(this).setTitle(R.string.app_name).setMessage(message) 1212 | .setPositiveButton(R.string.button_ok, null).setCancelable(false).show(); 1213 | } 1214 | } 1215 | 1216 | @Override 1217 | public boolean onPrepareOptionsMenu(Menu menu) { 1218 | menu.clear(); 1219 | if (mRecorder.state() == Recorder.RECORDING_STATE 1220 | || mRecorder.state() == Recorder.PLAYING_STATE) { 1221 | return false; 1222 | } else { 1223 | getMenuInflater().inflate(R.layout.view_list_menu, menu); 1224 | return true; 1225 | } 1226 | } 1227 | 1228 | @Override 1229 | public boolean onOptionsItemSelected(MenuItem item) { 1230 | Intent intent; 1231 | switch (item.getItemId()) { 1232 | case R.id.menu_fm: 1233 | saveSample(); 1234 | intent = new Intent(); 1235 | intent.addCategory(Intent.CATEGORY_DEFAULT); 1236 | intent.setData(Uri.parse("file://" + mRecorder.getRecordDir())); 1237 | startActivity(intent); 1238 | break; 1239 | case R.id.menu_setting: 1240 | intent = new Intent(this, SoundRecorderPreferenceActivity.class); 1241 | startActivity(intent); 1242 | break; 1243 | default: 1244 | break; 1245 | } 1246 | return true; 1247 | } 1248 | 1249 | private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { 1250 | private final int DELTA = SEEK_BAR_MAX / 20; 1251 | 1252 | private int mProgress = 0; 1253 | 1254 | private boolean mPlayingAnimation = false; 1255 | 1256 | private boolean mForwardAnimation = true; 1257 | 1258 | @Override 1259 | public void onStopTrackingTouch(SeekBar seekBar) { 1260 | stopAnimation(); 1261 | mRecorder.startPlayback((float) seekBar.getProgress() / SEEK_BAR_MAX); 1262 | } 1263 | 1264 | @Override 1265 | public void onStartTrackingTouch(SeekBar seekBar) { 1266 | mRecorder.pausePlayback(); 1267 | mPlayingAnimation = false; 1268 | } 1269 | 1270 | @Override 1271 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 1272 | if (fromUser) { 1273 | if (!mPlayingAnimation) { 1274 | mForwardAnimation = true; 1275 | startForwardAnimation(); 1276 | mPlayingAnimation = true; 1277 | mProgress = progress; 1278 | } 1279 | 1280 | if (progress >= mProgress + DELTA) { 1281 | if (!mForwardAnimation) { 1282 | mForwardAnimation = true; 1283 | stopAnimation(); 1284 | startForwardAnimation(); 1285 | } 1286 | mProgress = progress; 1287 | } else if (progress < mProgress - DELTA) { 1288 | if (mForwardAnimation) { 1289 | mForwardAnimation = false; 1290 | stopAnimation(); 1291 | startBackwardAnimation(); 1292 | } 1293 | mProgress = progress; 1294 | } 1295 | 1296 | setTimerView(((float) progress) / SEEK_BAR_MAX); 1297 | mLastButtonId = 0; 1298 | } 1299 | } 1300 | }; 1301 | 1302 | private class RecorderReceiver extends BroadcastReceiver { 1303 | 1304 | @Override 1305 | public void onReceive(Context context, Intent intent) { 1306 | if (intent.hasExtra(RecorderService.RECORDER_SERVICE_BROADCAST_STATE)) { 1307 | boolean isRecording = intent.getBooleanExtra( 1308 | RecorderService.RECORDER_SERVICE_BROADCAST_STATE, false); 1309 | mRecorder.setState(isRecording ? Recorder.RECORDING_STATE : Recorder.IDLE_STATE); 1310 | } else if (intent.hasExtra(RecorderService.RECORDER_SERVICE_BROADCAST_ERROR)) { 1311 | int error = intent.getIntExtra(RecorderService.RECORDER_SERVICE_BROADCAST_ERROR, 0); 1312 | mRecorder.setError(error); 1313 | } 1314 | } 1315 | } 1316 | } 1317 | -------------------------------------------------------------------------------- /src/net/micode/soundrecorder/SoundRecorderPreferenceActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.micode.soundrecorder; 18 | 19 | import android.content.Context; 20 | import android.content.SharedPreferences; 21 | import android.os.Bundle; 22 | import android.preference.PreferenceActivity; 23 | import android.preference.PreferenceManager; 24 | 25 | public class SoundRecorderPreferenceActivity extends PreferenceActivity { 26 | private static final String RECORD_TYPE = "pref_key_record_type"; 27 | 28 | private static final String ENABLE_HIGH_QUALITY = "pref_key_enable_high_quality"; 29 | 30 | private static final String ENABLE_SOUND_EFFECT = "pref_key_enable_sound_effect"; 31 | 32 | @Override 33 | protected void onCreate(Bundle icicle) { 34 | super.onCreate(icicle); 35 | addPreferencesFromResource(R.xml.preferences); 36 | } 37 | 38 | public static String getRecordType(Context context) { 39 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 40 | return settings.getString(RECORD_TYPE, context.getString(R.string.prefDefault_recordType)); 41 | } 42 | 43 | public static boolean isHighQuality(Context context) { 44 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 45 | return settings.getBoolean(ENABLE_HIGH_QUALITY, true); 46 | } 47 | 48 | public static boolean isEnabledSoundEffect(Context context) { 49 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 50 | return settings.getBoolean(ENABLE_SOUND_EFFECT, true); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/net/micode/soundrecorder/WheelImageView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.micode.soundrecorder; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.view.animation.Animation; 22 | import android.view.animation.LinearInterpolator; 23 | import android.widget.ImageView; 24 | 25 | public class WheelImageView extends ImageView { 26 | 27 | SeamlessAnimation mAnimation; 28 | 29 | public WheelImageView(Context context) { 30 | super(context); 31 | mAnimation = null; 32 | } 33 | 34 | public WheelImageView(Context context, AttributeSet attrs) { 35 | super(context, attrs); 36 | mAnimation = null; 37 | } 38 | 39 | public WheelImageView(Context context, AttributeSet attrs, int defStyle) { 40 | super(context, attrs, defStyle); 41 | mAnimation = null; 42 | } 43 | 44 | private void initAnimation(long duration, boolean isForward, int repeatCount) { 45 | LinearInterpolator lir = new LinearInterpolator(); 46 | float from; 47 | float to; 48 | if (isForward) { 49 | from = (mAnimation == null || mAnimation.getRepeatCount() != Animation.INFINITE) ? 0.0f 50 | : mAnimation.getDegree(); 51 | to = from + 360.0f; 52 | } else { 53 | from = (mAnimation == null || mAnimation.getRepeatCount() != Animation.INFINITE) ? 360.0f 54 | : mAnimation.getDegree(); 55 | to = from - 360.0f; 56 | } 57 | 58 | if (isForward) { 59 | mAnimation = new SeamlessAnimation(from, to, Animation.RELATIVE_TO_SELF, 0.5f, 60 | Animation.RELATIVE_TO_SELF, 0.5f); 61 | } else { 62 | mAnimation = new SeamlessAnimation(from, to, Animation.RELATIVE_TO_SELF, 0.5f, 63 | Animation.RELATIVE_TO_SELF, 0.5f); 64 | } 65 | mAnimation.setDuration(duration); 66 | mAnimation.setRepeatMode(Animation.RESTART); 67 | mAnimation.setRepeatCount(repeatCount); 68 | mAnimation.setInterpolator(lir); 69 | } 70 | 71 | public void startAnimation(long duration, boolean isForward) { 72 | startAnimation(duration, isForward, Animation.INFINITE); 73 | } 74 | 75 | public void startAnimation(long duration, boolean isForward, int repeatCount) { 76 | initAnimation(duration, isForward, repeatCount); 77 | startAnimation(mAnimation); 78 | } 79 | 80 | public void stopAnimation() { 81 | if (mAnimation != null) { 82 | mAnimation.cancel(); 83 | mAnimation = null; 84 | clearAnimation(); 85 | } 86 | } 87 | 88 | } 89 | --------------------------------------------------------------------------------