├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── fr │ │ └── s13d │ │ └── photobackup │ │ └── ApplicationTest.java │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── fr │ │ └── s13d │ │ └── photobackup │ │ ├── Log.java │ │ ├── PBActivity.java │ │ ├── PBApplication.java │ │ ├── PBBootBroadcastReceiver.java │ │ ├── PBConstants.java │ │ ├── PBService.java │ │ ├── PBWifiBroadcastReceiver.java │ │ ├── about │ │ └── PBAboutActivity.java │ │ ├── interfaces │ │ ├── PBMediaSenderInterface.java │ │ └── PBMediaStoreInterface.java │ │ ├── journal │ │ ├── PBJournalActivity.java │ │ └── PBJournalAdapter.java │ │ ├── media │ │ ├── PBMedia.java │ │ ├── PBMediaSender.java │ │ ├── PBMediaStore.java │ │ └── PBSyncMediaStoreTask.java │ │ └── preferences │ │ ├── PBPreferenceFragment.java │ │ ├── PBServerListPreference.java │ │ └── PBServerPreferenceFragment.java │ └── res │ ├── drawable-hdpi │ ├── ic_backup_white_48dp.png │ ├── ic_done_white_48dp.png │ ├── ic_error_outline_white_48dp.png │ └── ic_file_upload_white_24dp.png │ ├── drawable-mdpi │ ├── ic_backup_white_48dp.png │ ├── ic_done_white_48dp.png │ ├── ic_error_outline_white_48dp.png │ └── ic_file_upload_white_24dp.png │ ├── drawable-xhdpi │ ├── ic_backup_white_48dp.png │ ├── ic_done_white_48dp.png │ ├── ic_error_outline_white_48dp.png │ └── ic_file_upload_white_24dp.png │ ├── drawable-xxhdpi │ ├── ic_backup_white_48dp.png │ ├── ic_done_white_48dp.png │ ├── ic_error_outline_white_48dp.png │ └── ic_file_upload_white_24dp.png │ ├── drawable-xxxhdpi │ ├── ic_backup_white_48dp.png │ ├── ic_done_white_48dp.png │ ├── ic_error_outline_white_48dp.png │ └── ic_file_upload_white_24dp.png │ ├── drawable │ └── icon.png │ ├── layout │ ├── activity_about.xml │ ├── activity_journal.xml │ ├── list_row.xml │ └── server_list_row.xml │ ├── menu │ └── main_menu.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-ldpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-cs │ └── strings.xml │ ├── values-de │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-v11 │ └── styles.xml │ ├── values-v14 │ └── styles.xml │ ├── values-v21 │ └── styles.xml │ ├── values │ ├── arrays.xml │ ├── servers_params.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── preferences.xml │ └── server_preferences.xml ├── build.gradle ├── codecov.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | .classpath 20 | .project 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Gradle files 29 | .gradle/ 30 | build/ 31 | /*/build/ 32 | 33 | # Android Studio files 34 | .idea/ 35 | project.properties 36 | /*/local.properties 37 | /*/out 38 | /*/*/build 39 | build/ 40 | /*/*/production 41 | *.iml 42 | *.iws 43 | *.ipr 44 | *~ 45 | *.swp 46 | 47 | # Mac oS X 48 | .DS_Store 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk8 3 | # Turn off caching to avoid any caching problems 4 | cache: false 5 | # Use the Travis Container-Based Infrastructure 6 | sudo: false 7 | env: 8 | global: 9 | - ANDROID_API_LEVEL=25 10 | - ANDROID_BUILD_TOOLS_VERSION=25.0.0 11 | - ANDROID_ABI=armeabi-v7a 12 | - ADB_INSTALL_TIMEOUT=10 # minutes (2 minutes by default) 13 | 14 | android: 15 | components: 16 | - tools 17 | - build-tools-$ANDROID_BUILD_TOOLS_VERSION 18 | - android-$ANDROID_API_LEVEL 19 | - extra-android-m2repository 20 | # Support library 21 | - extra-android-support 22 | # Specify at least one system image 23 | - sys-img-armeabi-v7a-android-$ANDROID_API_LEVEL 24 | 25 | before_script: 26 | # Create emulator 27 | - echo no | android create avd --force -n test -t android-$ANDROID_API_LEVEL --abi $ANDROID_ABI 28 | # Start emulator 29 | - emulator -avd test -no-skin -no-audio -no-window & 30 | - adb wait-for-device & 31 | - adb shell input keyevent 82 & 32 | # inject Falcon dependency (to take screenshots) 33 | # - sed -i "s/^dependencies {/dependencies { compile 'com.jraska:falcon:1.0.1'/g" build.gradle 34 | # - sed -i "s/^repositories {/repositories { jcenter()/g" build.gradle 35 | 36 | script: 37 | # Build the app 38 | - chmod +x gradlew 39 | - ./gradlew build -PdisablePreDex 40 | # - ./gradlew connectedAndroidTest 41 | 42 | # after_script: 43 | # - adb install build/outputs/apk/client-android-debug.apk 44 | # - adb shell am start -n fr.s13d.photobackup/.PBActivity 45 | # Go to About activity 46 | # - adb shell input tap 500 1300 47 | # - bash <(curl -s https://codecov.io/bash) 48 | 49 | 50 | # gitter integration 51 | notifications: 52 | webhooks: 53 | urls: 54 | - https://webhooks.gitter.im/e/78e5e3847120190ddae2 55 | on_success: change # options: [always|never|change] default: always 56 | on_failure: always # options: [always|never|change] default: always 57 | on_start: never # options: [always|never|change] default: always 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # 0.9.0 4 | * [4a9e39c](https://github.com/PhotoBackup/client-android/commit/4a9e39c) Switch from loopj to OkHttp (to handle HTTPS SNI #23 and redirection #15) ; 5 | * [67eeb00](https://github.com/PhotoBackup/client-android/commit/67eeb00) Async update of Upload Log ui after manual upload ; 6 | * [71e7dee](https://github.com/PhotoBackup/client-android/commit/71e7dee) Saving the error message per photo and display it under the file name ; 7 | * [c5cfe04](https://github.com/PhotoBackup/client-android/commit/c5cfe04) New German translation thanks to @zealot128 ; 8 | * [0892104](https://github.com/PhotoBackup/client-android/commit/0892104) New Czech translation thanks to @svetlemodry ; 9 | * [ca629b3](https://github.com/PhotoBackup/client-android/commit/ca629b3) handling 409 status code properly #45 ; 10 | * [1108063](https://github.com/PhotoBackup/client-android/commit/1108063) remove 'Stop the service' action in the notification #41 ; 11 | * some Sonar-induced fixes ([c8ccce8](https://github.com/PhotoBackup/client-android/commit/c8ccce8), [af13b2c](https://github.com/PhotoBackup/client-android/commit/af13b2c), [f0f8508](https://github.com/PhotoBackup/client-android/commit/f0f8508) and [8ce3611](https://github.com/PhotoBackup/client-android/commit/8ce3611)) ; 12 | * finally, an operational Travis build: https://travis-ci.org/PhotoBackup/client-android ; 13 | * for developers, a gitter channel to discuss: https://gitter.im/PhotoBackup 14 | 15 | 16 | # 0.8.0 17 | * [3c4f128](https://github.com/PhotoBackup/client-android/commit/3c4f128) Czech translation 18 | * [47a0c62](https://github.com/PhotoBackup/client-android/commit/47a0c62) Add multiple servers capability basic architecture 19 | 20 | ## 0.7.2 21 | * [dae700c](https://github.com/PhotoBackup/client-android/commit/dae700c) Fix 0.6 => 0.7 migration by setting it before addPreferencesFromResources 22 | 23 | ## 0.7.1 24 | * [29ac3dc](https://github.com/PhotoBackup/client-android/commit/29ac3dc) Remove Handler messages for recycled views 25 | * [2080fc0](https://github.com/PhotoBackup/client-android/commit/2080fc0) Fix 'Only wifi' preference type bug in v0.7.0 by adding a migration 26 | 27 | ## 0.7.0 28 | * [c5f5cab](https://github.com/PhotoBackup/client-android/commit/c5f5cab) Add an option to upload only recently taken pictures. 29 | * [4321481](https://github.com/PhotoBackup/client-android/commit/4321481) Add filter to journal activity 30 | * [7cf98d5](https://github.com/PhotoBackup/client-android/commit/7cf98d5) Set delay to new photos up to 10 minutes 31 | * [a07628f](https://github.com/PhotoBackup/client-android/commit/a07628f) Fix erroneous number of pictures in journal entry 32 | 33 | ## 0.6.5 34 | * [b90deb4](https://github.com/PhotoBackup/client-android/commit/b90deb4) Support Android M runtime permissions 35 | * [b7bf565](https://github.com/PhotoBackup/client-android/commit/b7bf565) Update AAHC to version 1.4.9 36 | * [84e540a](https://github.com/PhotoBackup/client-android/commit/84e540a) Move permission callback from fragment to activity 37 | 38 | ## 0.6.4 39 | * [47e5ce3](https://github.com/PhotoBackup/client-android/commit/47e5ce3) Add notification action to stop the service from it. 40 | * [4da4364](https://github.com/PhotoBackup/client-android/commit/4da4364) Update for Android M 41 | 42 | ## 0.6.3 43 | * [2dd370a](https://github.com/PhotoBackup/client-android/commit/2dd370a) Fix bug: boot receiver not reading user-defined prefernce 44 | 45 | ## 0.6.2 46 | * [eada898](https://github.com/PhotoBackup/client-android/commit/eada898) Fix bug #3 by removing slashes at the end of URLs 47 | * [8b37bbb](https://github.com/PhotoBackup/client-android/commit/8b37bbb) Fix #2: set network choosing as a modal single choice dialog 48 | * [a689da7](https://github.com/PhotoBackup/client-android/commit/a689da7) Update error icon 49 | 50 | ## 0.6.1 51 | * [5e368d0](https://github.com/PhotoBackup/client-android/commit/5e368d0) Fix password hash when changing password 52 | * [b5ef6d4](https://github.com/PhotoBackup/client-android/commit/b5ef6d4) Add screenshot image 53 | * [c0aeccb](https://github.com/PhotoBackup/client-android/commit/c0aeccb) Remove unused text 54 | * [10f0faf](https://github.com/PhotoBackup/client-android/commit/10f0faf) Add README.md 55 | 56 | ## 0.6 57 | * [163f604](https://github.com/PhotoBackup/client-android/commit/163f604) Add an 'About' activity to explain quickly the purpose of the app and 58 | 59 | ## 0.5 60 | * [7747624](https://github.com/PhotoBackup/client-android/commit/7747624) Add file size in the request parameter 61 | 62 | ## 0.4 63 | * [6e1ca37](https://github.com/PhotoBackup/client-android/commit/6e1ca37) Initial commit 64 | 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/PhotoBackup/client-android.svg?branch=master)](https://travis-ci.org/PhotoBackup/client-android) 2 | # PhotoBackup Android client 3 | 4 | The PhotoBackup Android application is a simple service always running 5 | silently in background and sending photos to the associated server 6 | as soon as you take them. An upload journal allows you to see the status 7 | of each photo in your device. It does not backup videos. 8 | 9 | It is available on the 10 | [Google Play](https://play.google.com/store/apps/details?id=fr.s13d.photobackup) 11 | and on [F-Droid](https://f-droid.org/app/fr.s13d.photobackup). 12 | 13 | It does not use services like Google Drive/Dropbox/SkyDrive/... **on purpose**, 14 | as you don't know what they do with your photos. With PhotoBackup, only you should do. 15 | 16 | Join our dedicated online chat at [![Gitter](https://badges.gitter.im/PhotoBackup/client-android.svg)](https://gitter.im/PhotoBackup/client-android). 17 | 18 | Screenshot 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | ext.supportLibVersion = '25.0.0' 4 | 5 | 6 | android { 7 | compileSdkVersion 25 8 | buildToolsVersion "24.0.3" 9 | dataBinding.enabled = true 10 | 11 | defaultConfig { 12 | applicationId "fr.s13d.photobackup" 13 | minSdkVersion 16 14 | targetSdkVersion 25 15 | versionCode 33 16 | versionName "0.11.0" 17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 18 | } 19 | buildTypes { 20 | debug { 21 | debuggable true 22 | minifyEnabled false 23 | applicationIdSuffix ".debug" 24 | versionNameSuffix '-dev' 25 | } 26 | release { 27 | debuggable false 28 | minifyEnabled true 29 | shrinkResources true 30 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 31 | } 32 | } 33 | lintOptions { 34 | abortOnError false 35 | } 36 | 37 | // to force Gradle version conflicts resolving 38 | configurations.all { 39 | resolutionStrategy.force 'com.google.code.findbugs:jsr305:2.0.1' 40 | } 41 | } 42 | 43 | dependencies { 44 | // compile 45 | compile fileTree(dir: 'libs', include: ['*.jar']) 46 | compile "com.android.support:appcompat-v7:${supportLibVersion}" 47 | compile 'com.squareup.okhttp3:okhttp:3.2.0' 48 | compile('de.psdev.licensesdialog:licensesdialog:1.8.1') 49 | 50 | // jUnit 51 | testCompile 'junit:junit:4.12' 52 | 53 | // androidTestCompile 54 | androidTestCompile 'com.android.support.test:runner:0.5' 55 | androidTestCompile 'com.android.support.test:rules:0.5' 56 | androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' 57 | androidTestCompile "com.android.support:appcompat-v7:${supportLibVersion}" 58 | } 59 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/stephane/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # do not print notes 20 | -dontnote 21 | 22 | # keep attributes 23 | -keepattributes EnclosingMethod 24 | 25 | # keep class from included classes 26 | -dontwarn java.nio.file.** 27 | -keep class java.nio.file.** { *; } 28 | 29 | -dontwarn org.codehaus.mojo.** 30 | -keep class org.codehaus.mojo.** { *; } 31 | -------------------------------------------------------------------------------- /app/src/androidTest/java/fr/s13d/photobackup/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup; 20 | 21 | import android.support.test.InstrumentationRegistry; 22 | import android.support.test.filters.LargeTest; 23 | import android.support.test.rule.ActivityTestRule; 24 | import android.support.test.runner.AndroidJUnit4; 25 | 26 | import org.junit.Rule; 27 | import org.junit.Test; 28 | import org.junit.runner.RunWith; 29 | 30 | import static android.support.test.espresso.Espresso.onView; 31 | import static android.support.test.espresso.action.ViewActions.click; 32 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 33 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 34 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 35 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 36 | 37 | 38 | @RunWith(AndroidJUnit4.class) 39 | @LargeTest 40 | public class ApplicationTest { 41 | 42 | @Rule 43 | public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(PBActivity.class); 44 | 45 | @Test 46 | public void startMainActivityShouldStartWithBasicContent() { 47 | // this is the PhotoBackup activity, it contains its name 48 | onView(withText(R.string.app_name)).check(matches(isDisplayed())); 49 | 50 | // Service is not running at start 51 | onView(withText(R.string.service_title)).check(matches(isDisplayed())); 52 | onView(withText(R.string.service_state_not_running)).check(matches(isDisplayed())); 53 | 54 | onView(withText(R.string.server_pref_title)).check(matches(isDisplayed())); 55 | } 56 | 57 | 58 | @Test 59 | public void clickOnAboutPreferenceShouldOpenAboutActivity() { 60 | onView(withText(R.string.app_name)).check(matches(isDisplayed())); 61 | 62 | // Service is not running at start 63 | onView(withText(R.string.service_state_not_running)).check(matches(isDisplayed())); 64 | 65 | onView(withText(R.string.about_title)).check(matches(isDisplayed())); 66 | 67 | // open About activity 68 | onView(withText(R.string.about_title)).perform(click()); 69 | 70 | // check that About activity is opened 71 | String aboutText = InstrumentationRegistry.getTargetContext().getString(R.string.about_text); 72 | onView(withId(R.id.aboutTextTextView)).check(matches(withText(aboutText))); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | PB Debug 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/Log.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup; 20 | 21 | 22 | /** 23 | * Own logger class to avoid leaking logs in release mode. 24 | */ 25 | public final class Log { 26 | 27 | private Log() throws InstantiationException { 28 | throw new InstantiationException("This class is not meant to be instantiated!"); 29 | } 30 | 31 | 32 | /** 33 | * Log a debug message 34 | * @param tag tag of the logged message 35 | * @param message message logged 36 | */ 37 | public static void d(final String tag, final String message) { 38 | if (BuildConfig.DEBUG) { 39 | android.util.Log.d(tag, message); 40 | } 41 | } 42 | 43 | 44 | /** 45 | * Log a debug exception 46 | * @param tag tag of the logged exception 47 | * @param e exception logged 48 | */ 49 | public static void d(final String tag, final Exception e) { 50 | if (BuildConfig.DEBUG) { 51 | android.util.Log.d(tag, e.toString()); 52 | } 53 | } 54 | 55 | 56 | /** 57 | * Log an error message 58 | * @param tag tag of the logged message 59 | * @param message message logged 60 | */ 61 | public static void e(final String tag, final String message) { 62 | if (BuildConfig.DEBUG) { 63 | android.util.Log.e(tag, message); 64 | } 65 | } 66 | 67 | 68 | /** 69 | * Log an error exception 70 | * @param tag tag of the logged exception 71 | * @param e exception logged 72 | */ 73 | public static void e(final String tag, final Exception e) { 74 | if (BuildConfig.DEBUG) { 75 | android.util.Log.e(tag, e.toString()); 76 | } 77 | } 78 | 79 | 80 | /** 81 | * Log an info message 82 | * @param tag tag of the logged message 83 | * @param message message logged 84 | */ 85 | public static void i(final String tag, final String message) { 86 | if (BuildConfig.DEBUG) { 87 | android.util.Log.i(tag, message); 88 | } 89 | } 90 | 91 | 92 | /** 93 | * Log an info exception 94 | * @param tag tag of the logged exception 95 | * @param e exception logged 96 | */ 97 | public static void i(final String tag, final Exception e) { 98 | if (BuildConfig.DEBUG) { 99 | android.util.Log.i(tag, e.toString()); 100 | } 101 | } 102 | 103 | 104 | /** 105 | * Log a verbose message 106 | * @param tag tag of the logged message 107 | * @param message message logged 108 | */ 109 | public static void v(final String tag, final String message) { 110 | if (BuildConfig.DEBUG) { 111 | android.util.Log.v(tag, message); 112 | } 113 | } 114 | 115 | 116 | /** 117 | * Log a verbose exception 118 | * @param tag tag of the logged exception 119 | * @param e exception logged 120 | */ 121 | public static void v(final String tag, final Exception e) { 122 | if (BuildConfig.DEBUG) { 123 | android.util.Log.v(tag, e.toString()); 124 | } 125 | } 126 | 127 | 128 | /** 129 | * Log a warning message 130 | * @param tag tag of the logged message 131 | * @param message message logged 132 | */ 133 | public static void w(final String tag, final String message) { 134 | if (BuildConfig.DEBUG) { 135 | android.util.Log.w(tag, message); 136 | } 137 | } 138 | 139 | 140 | /** 141 | * Log a warning exception 142 | * @param tag tag of the logged exception 143 | * @param e exception logged 144 | */ 145 | public static void w(final String tag, final Exception e) { 146 | if (BuildConfig.DEBUG) { 147 | android.util.Log.w(tag, e.toString()); 148 | } 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/PBActivity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup; 20 | 21 | import android.app.Activity; 22 | import android.app.NotificationManager; 23 | import android.content.Context; 24 | import android.content.pm.PackageManager; 25 | import android.os.Bundle; 26 | import android.support.annotation.NonNull; 27 | import android.view.MenuItem; 28 | import android.widget.Toast; 29 | 30 | import fr.s13d.photobackup.preferences.PBPreferenceFragment; 31 | 32 | public class PBActivity extends Activity { 33 | 34 | private static final PBPreferenceFragment preferenceFragment = new PBPreferenceFragment(); 35 | 36 | 37 | ////////////// 38 | // Override // 39 | ////////////// 40 | @Override 41 | protected void onCreate(final Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | getFragmentManager().beginTransaction().replace(android.R.id.content, preferenceFragment).commit(); 44 | setActionBar(); 45 | } 46 | 47 | 48 | @Override 49 | protected void onStart() { 50 | super.onStart(); 51 | final NotificationManager nm = (NotificationManager) PBApplication.getApp().getSystemService(Context.NOTIFICATION_SERVICE); 52 | nm.cancelAll(); 53 | } 54 | 55 | 56 | @Override 57 | public void onRequestPermissionsResult(int requestCode, 58 | @NonNull String[] permissions, 59 | @NonNull int[] grantResults) { 60 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 61 | 62 | if (requestCode == PBPreferenceFragment.PERMISSION_READ_EXTERNAL_STORAGE) { 63 | if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 64 | Log.i("PBPreferenceFragment", "READ_EXTERNAL_STORAGE permission granted."); 65 | preferenceFragment.didGrantPermission(); // continue to next step 66 | } else { 67 | Log.i("PBPreferenceFragment", "READ_EXTERNAL_STORAGE was NOT granted."); 68 | Toast.makeText(this, R.string.toast_permission_not_granted, Toast.LENGTH_LONG).show(); 69 | } 70 | } 71 | } 72 | 73 | 74 | @Override 75 | public boolean onOptionsItemSelected(MenuItem item) { 76 | if (item.getItemId() == android.R.id.home) { 77 | getFragmentManager().popBackStackImmediate(); 78 | setActionBar(); 79 | } 80 | return super.onOptionsItemSelected(item); 81 | } 82 | 83 | 84 | ///////////// 85 | // methods // 86 | ///////////// 87 | public void setActionBar() { 88 | // title and back button of the action bar 89 | setTitle(R.string.app_name); 90 | if (getActionBar() != null) { 91 | getActionBar().setDisplayHomeAsUpEnabled(false); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/PBApplication.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | *

4 | * This file is part of PhotoBackup. 5 | *

6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | *

11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | *

16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup; 20 | 21 | import android.app.Application; 22 | import android.preference.PreferenceManager; 23 | 24 | import java.util.Map; 25 | 26 | import fr.s13d.photobackup.media.PBMediaStore; 27 | 28 | 29 | /** 30 | * Application of the app 31 | */ 32 | public class PBApplication extends Application { 33 | 34 | private static final String LOG_TAG = "PBApplication"; 35 | private static PBApplication app; 36 | private static PBMediaStore mediaStore; 37 | private static final Object lock = new Object(); 38 | 39 | 40 | /////////////// 41 | // Constants // 42 | /////////////// 43 | public static final String PB_USER_AGENT = "PhotoBackup Android Client v" + BuildConfig.VERSION_NAME; 44 | public static final String PB_MEDIAS_SHARED_PREFS = "PB_MEDIAS_SHARED_PREFS"; 45 | 46 | 47 | //////////////// 48 | // Life-cycle // 49 | //////////////// 50 | @Override 51 | public void onCreate() { 52 | super.onCreate(); 53 | Log.d(LOG_TAG, "Initializing app"); 54 | setApp(this); 55 | 56 | if (BuildConfig.DEBUG) { 57 | Log.d(LOG_TAG, "Default SharedPreferences:"); 58 | final Map allPrefs = PreferenceManager.getDefaultSharedPreferences(this).getAll(); 59 | for (final Map.Entry entry : allPrefs.entrySet()) { 60 | Log.d(LOG_TAG, entry.getKey() + "<" + entry.getValue().getClass().getSimpleName() + "> = " 61 | + entry.getValue().toString()); 62 | } 63 | } 64 | } 65 | 66 | 67 | @Override 68 | public void onLowMemory() { 69 | super.onLowMemory(); 70 | trimMemory(); 71 | } 72 | 73 | 74 | @Override 75 | public void onTrimMemory(int level) { 76 | super.onTrimMemory(level); 77 | trimMemory(); 78 | } 79 | 80 | 81 | private void trimMemory() { 82 | Log.d(LOG_TAG, "trimMemory"); 83 | if (mediaStore != null) { 84 | mediaStore.close(); 85 | nullifyMediaStore(); 86 | } 87 | } 88 | 89 | 90 | ///////////////////// 91 | // Getters/setters // 92 | ///////////////////// 93 | public static PBApplication getApp() { 94 | return app; 95 | } 96 | 97 | 98 | private static void setApp(PBApplication application) { 99 | app = application; 100 | } 101 | 102 | 103 | public static PBMediaStore getMediaStore() { 104 | synchronized (lock) { 105 | if (mediaStore == null) { 106 | mediaStore = new PBMediaStore(); 107 | mediaStore.sync(); 108 | } 109 | } 110 | return mediaStore; 111 | } 112 | 113 | /** 114 | * Nullify the media store when destroying it. 115 | */ 116 | public static void nullifyMediaStore() { 117 | synchronized (lock) { 118 | mediaStore = null; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/PBBootBroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package fr.s13d.photobackup; 21 | 22 | import android.content.BroadcastReceiver; 23 | import android.content.Context; 24 | import android.content.Intent; 25 | import android.content.SharedPreferences; 26 | import android.preference.PreferenceManager; 27 | 28 | 29 | /** 30 | * Receives boot broadcast intents and start PB service if required. 31 | */ 32 | public class PBBootBroadcastReceiver extends BroadcastReceiver { 33 | 34 | @Override 35 | public void onReceive(final Context context, final Intent intent) { 36 | 37 | if (intent != null && "android.intent.action.BOOT_COMPLETED".equals(intent.getAction())) { 38 | final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 39 | final boolean shouldRun = preferences.getBoolean(PBConstants.PREF_SERVICE_RUNNING, false); 40 | if (shouldRun) { 41 | final Intent startServiceIntent = new Intent(context, PBService.class); 42 | context.startService(startServiceIntent); 43 | } 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/PBConstants.java: -------------------------------------------------------------------------------- 1 | package fr.s13d.photobackup; 2 | 3 | 4 | /** 5 | * Delivers all public constants to the application. 6 | */ 7 | public class PBConstants { 8 | 9 | // should correspond to what is in preferences.xml 10 | public static final String PREF_SERVICE_RUNNING = "PREF_SERVICE_RUNNING"; 11 | public static final String PREF_SERVER = "PREF_SERVER"; 12 | public static final String PREF_WIFI_ONLY = "PREF_WIFI_ONLY"; 13 | public static final String PREF_MEDIAS_TO_BACKUP = "PREF_MEDIAS_TO_BACKUP"; 14 | public static final String PREF_RECENT_UPLOAD_ONLY = "PREF_RECENT_UPLOAD_ONLY"; 15 | public static final String PREF_PICTURE_FOLDER_LIST = "PREF_PICTURE_FOLDER_LIST"; 16 | public static final String PREF_MEDIA_BACKUP_VIDEO = "PREF_MEDIA_BACKUP_VIDEO"; 17 | public static final String PREF_UPLOAD_JOURNAL = "PREF_UPLOAD_JOURNAL"; 18 | public static final String PREF_ABOUT = "PREF_ABOUT"; 19 | 20 | // origin to permission asking, to be able to reroute 21 | public static final int PERM_ORIGIN_SERVICE = 0; 22 | public static final int PERM_ORIGIN_MEDIAS = 1; 23 | 24 | 25 | // Prevent from constructing objects of this class 26 | private PBConstants() { throw new AssertionError(); } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/PBService.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup; 20 | 21 | import android.app.Service; 22 | import android.content.Intent; 23 | import android.content.SharedPreferences; 24 | import android.database.ContentObserver; 25 | import android.net.Uri; 26 | import android.os.IBinder; 27 | import android.preference.PreferenceManager; 28 | import android.provider.MediaStore; 29 | 30 | import fr.s13d.photobackup.interfaces.PBMediaSenderInterface; 31 | import fr.s13d.photobackup.interfaces.PBMediaStoreInterface; 32 | import fr.s13d.photobackup.media.PBMedia; 33 | import fr.s13d.photobackup.media.PBMediaSender; 34 | 35 | 36 | /** 37 | * Main background service doing the whole job of listening 38 | * to new media being saved and initiating media uploads. 39 | */ 40 | public class PBService extends Service implements PBMediaStoreInterface, PBMediaSenderInterface { 41 | 42 | private static final String LOG_TAG = "PBService"; 43 | private static MediaContentObserver imagesContentObserver; 44 | private static MediaContentObserver videosContentObserver; 45 | private PBMediaSender mediaSender; 46 | private Binder binder; 47 | 48 | 49 | ////////////// 50 | // Override // 51 | ////////////// 52 | @Override 53 | public void onCreate() { 54 | super.onCreate(); 55 | binder = new Binder(); 56 | setImagesContentObserver(new MediaContentObserver()); 57 | this.getApplicationContext().getContentResolver().registerContentObserver( 58 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, imagesContentObserver); 59 | setVideosContentObserver(new MediaContentObserver()); 60 | this.getApplicationContext().getContentResolver().registerContentObserver( 61 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI, false, videosContentObserver); 62 | PBApplication.getMediaStore().addInterface(this); 63 | 64 | Log.i(LOG_TAG, "PhotoBackup service is created"); 65 | } 66 | 67 | 68 | @Override 69 | public void onDestroy() { 70 | super.onDestroy(); 71 | this.getApplicationContext().getContentResolver().unregisterContentObserver(imagesContentObserver); 72 | this.getApplicationContext().getContentResolver().unregisterContentObserver(videosContentObserver); 73 | setImagesContentObserver(null); 74 | setVideosContentObserver(null); 75 | PBApplication.getMediaStore().removeInterface(this); 76 | PBApplication.nullifyMediaStore(); 77 | 78 | Log.i(LOG_TAG, "PhotoBackup service has stopped"); 79 | } 80 | 81 | 82 | @Override 83 | public int onStartCommand(final Intent intent, final int flags, final int startId) { 84 | super.onStartCommand(intent, flags, startId); 85 | Log.i(LOG_TAG, "PhotoBackup service has started"); 86 | return START_STICKY; 87 | } 88 | 89 | 90 | @Override 91 | public IBinder onBind(Intent intent) { 92 | return binder; 93 | } 94 | 95 | 96 | /** 97 | * Binder class 98 | */ 99 | public class Binder extends android.os.Binder { 100 | public PBService getService() { 101 | return PBService.this; 102 | } 103 | } 104 | 105 | 106 | ///////////// 107 | // Methods // 108 | ///////////// 109 | /** 110 | * Sends next media in store. 111 | */ 112 | public void sendNextMedia() { 113 | final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(PBApplication.getApp()); 114 | final boolean isRunning = preferences.getBoolean(PBConstants.PREF_SERVICE_RUNNING, false); 115 | if (isRunning) { 116 | for (PBMedia media : PBApplication.getMediaStore().getMediaList()) { 117 | if (media.getState() != PBMedia.PBMediaState.SYNCED) { 118 | sendMedia(media, false); 119 | break; 120 | } 121 | } 122 | } 123 | } 124 | 125 | 126 | /** 127 | * Initiates a media media upload. 128 | * @param media media to be sent 129 | * @param manual indicates if upload is manually or automatically (through sendNextMedia()) initiated. 130 | */ 131 | public void sendMedia(PBMedia media, boolean manual) { 132 | getMediaSender().send(media, manual); 133 | } 134 | 135 | 136 | //////////////////////////////////// 137 | // PBMediaStoreListener callbacks // 138 | //////////////////////////////////// 139 | /** 140 | * Sends next media on media store synchronisation. 141 | */ 142 | public void onSyncMediaStoreTaskPostExecute() { 143 | sendNextMedia(); 144 | } 145 | 146 | 147 | ////////////////////////////////////// 148 | // PBMediaSenderInterface callbacks // 149 | ////////////////////////////////////// 150 | /** 151 | * Does nothing on receiving a message. 152 | * @param message received message 153 | */ 154 | public void onMessage(final String message) { 155 | // Do nothing 156 | } 157 | 158 | 159 | /** 160 | * Send next media on sending with success. 161 | */ 162 | public void onSendSuccess() { 163 | sendNextMedia(); 164 | } 165 | 166 | 167 | /** 168 | * Does nothing on sending with failure. 169 | */ 170 | public void onSendFailure() { 171 | // Do nothing 172 | } 173 | 174 | 175 | /** 176 | * Does nothing on test success. 177 | */ 178 | public void onTestSuccess() { 179 | // Do nothing 180 | } 181 | 182 | 183 | /** 184 | * Does nothing on test failure. 185 | */ 186 | public void onTestFailure() { 187 | // Do nothing 188 | } 189 | 190 | 191 | ///////////////////////////////////////////////////////////// 192 | // ContentObserver to react on the creation of a new media // 193 | ///////////////////////////////////////////////////////////// 194 | private class MediaContentObserver extends ContentObserver { 195 | 196 | private MediaContentObserver() { 197 | super(null); 198 | } 199 | 200 | @Override 201 | public void onChange(boolean selfChange, Uri uri) { 202 | super.onChange(selfChange); 203 | Log.i(LOG_TAG, "MediaContentObserver:onChange()"); 204 | 205 | final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(PBService.this); 206 | final boolean backupVideos = sp.getBoolean(PBConstants.PREF_MEDIA_BACKUP_VIDEO, false); 207 | if ("content://media/external/images/media".equals(uri.toString()) || 208 | (backupVideos && "content://media/external/video/media".equals(uri.toString()))) { 209 | 210 | try { 211 | final PBMedia media = PBApplication.getMediaStore().createMediaForLatestInStore(backupVideos); 212 | if (media != null) { 213 | media.setState(PBMedia.PBMediaState.WAITING); 214 | getMediaSender().send(media, false); 215 | } 216 | } 217 | catch (Exception e) { 218 | Log.e(LOG_TAG, "Upload failed :-("); 219 | Log.w(LOG_TAG, e); 220 | } 221 | } 222 | } 223 | } 224 | 225 | 226 | ///////////////////// 227 | // getters/setters // 228 | ///////////////////// 229 | private PBMediaSender getMediaSender() { 230 | if (mediaSender == null) { 231 | mediaSender = new PBMediaSender(); 232 | mediaSender.addInterface(this); 233 | } 234 | return mediaSender; 235 | } 236 | 237 | 238 | private static void setVideosContentObserver(MediaContentObserver contentObserver) { 239 | videosContentObserver = contentObserver; 240 | } 241 | 242 | 243 | private static void setImagesContentObserver(MediaContentObserver contentObserver) { 244 | imagesContentObserver = contentObserver; 245 | } 246 | 247 | 248 | } 249 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/PBWifiBroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | *

4 | * This file is part of PhotoBackup. 5 | *

6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | *

11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | *

16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package fr.s13d.photobackup; 21 | 22 | import android.content.BroadcastReceiver; 23 | import android.content.Context; 24 | import android.content.Intent; 25 | import android.content.SharedPreferences; 26 | import android.net.wifi.WifiManager; 27 | import android.os.IBinder; 28 | import android.preference.PreferenceManager; 29 | 30 | import java.util.List; 31 | 32 | import fr.s13d.photobackup.media.PBMedia; 33 | 34 | 35 | /** 36 | * Receives wifi broadcast intents and starts media upload if required. 37 | */ 38 | public class PBWifiBroadcastReceiver extends BroadcastReceiver { 39 | private static final String LOG_TAG = "PBWifiBroadcastReceiver"; 40 | private static long lastFiredOn = 0; 41 | 42 | 43 | @Override 44 | public void onReceive(final Context context, final Intent intent) { 45 | // Receiver should only live a short time, but the onReceive is called multiple times 46 | // Only accept reception every 10 minutes 47 | final long now = System.currentTimeMillis() / 1000; 48 | if (now - getLastFiredOn() < 600) { 49 | return; 50 | } 51 | 52 | final WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); 53 | final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 54 | final String wifiOnlyString = preferences.getString(PBConstants.PREF_WIFI_ONLY, context.getResources().getString(R.string.only_wifi_default)); 55 | final Boolean wifiOnly = wifiOnlyString.equals(context.getResources().getString(R.string.only_wifi)); 56 | 57 | Log.i(LOG_TAG, "New intent: action=" + intent.getAction() + ", type=" + intent.getType()); 58 | if (wifiManager.isWifiEnabled() && wifiOnly) { 59 | handleWifi(context, now); 60 | } 61 | } 62 | 63 | 64 | private void handleWifi(final Context context, final long now) { 65 | Log.i(LOG_TAG, "Wifi comes back, checking Service"); 66 | final IBinder binder = peekService(context, new Intent(context, PBService.class)); 67 | if (binder == null) { 68 | return; 69 | } 70 | final PBService service = ((PBService.Binder) binder).getService(); 71 | if (service == null) { 72 | return; 73 | } 74 | setLastFiredOn(now); 75 | 76 | final List medias = PBApplication.getMediaStore().getMediaList(); 77 | Log.i(LOG_TAG, "media count = " + medias.size()); 78 | 79 | for (final PBMedia media : medias) { 80 | if (media.getAge() < 3600 * 24 * 7 && media.getState() != PBMedia.PBMediaState.SYNCED) { 81 | Log.i(LOG_TAG, "Notify to send " + media.getPath()); 82 | service.sendMedia(media, true); 83 | } 84 | } 85 | } 86 | 87 | 88 | ///////////////////// 89 | // getters/setters // 90 | ///////////////////// 91 | private static long getLastFiredOn() { 92 | return lastFiredOn; 93 | } 94 | 95 | private static void setLastFiredOn(long lastFiredOn) { 96 | PBWifiBroadcastReceiver.lastFiredOn = lastFiredOn; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/about/PBAboutActivity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup.about; 20 | 21 | import android.app.Activity; 22 | import android.databinding.DataBindingUtil; 23 | import android.os.Bundle; 24 | import android.view.View; 25 | 26 | import de.psdev.licensesdialog.LicensesDialog; 27 | import de.psdev.licensesdialog.licenses.ApacheSoftwareLicense20; 28 | import de.psdev.licensesdialog.model.Notice; 29 | import de.psdev.licensesdialog.model.Notices; 30 | import fr.s13d.photobackup.BuildConfig; 31 | import fr.s13d.photobackup.R; 32 | import fr.s13d.photobackup.databinding.ActivityAboutBinding; 33 | 34 | 35 | public class PBAboutActivity extends Activity { 36 | 37 | @Override 38 | protected void onCreate(final Bundle savedInstanceState) { 39 | super.onCreate(savedInstanceState); 40 | 41 | // Set up the UI (with binding) 42 | ActivityAboutBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_about); 43 | String versionString = BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ')'; 44 | binding.versionTextView.setText(getString(R.string.app_version, versionString)); 45 | } 46 | 47 | 48 | public void clickOnOpenSourceLicenses(final View view) throws Exception { 49 | final Notices notices = new Notices(); 50 | notices.addNotice(new Notice( 51 | "Android v7 Support Libraries", 52 | "https://developer.android.com/topic/libraries/support-library/features.html#v7", 53 | "Copyright (C) 2012 The Android Open Source Project", 54 | new ApacheSoftwareLicense20())); 55 | notices.addNotice(new Notice( 56 | "LicensesDialog", 57 | "http://psdev.de", 58 | "Copyright 2013 Philip Schiffer ", 59 | new ApacheSoftwareLicense20())); 60 | notices.addNotice(new Notice( 61 | "OkHttp", 62 | "http://square.github.io/okhttp/", 63 | "Copyright 2016 Square, Inc.", 64 | new ApacheSoftwareLicense20())); 65 | 66 | new LicensesDialog.Builder(this) 67 | .setNotices(notices) 68 | .build() 69 | .show(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/interfaces/PBMediaSenderInterface.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup.interfaces; 20 | 21 | 22 | public interface PBMediaSenderInterface { 23 | 24 | void onMessage(final String message); 25 | 26 | void onSendSuccess(); 27 | void onSendFailure(); 28 | 29 | void onTestSuccess(); 30 | void onTestFailure(); 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/interfaces/PBMediaStoreInterface.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package fr.s13d.photobackup.interfaces; 21 | 22 | 23 | public interface PBMediaStoreInterface { 24 | void onSyncMediaStoreTaskPostExecute(); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/journal/PBJournalActivity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | *

4 | * This file is part of PhotoBackup. 5 | *

6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | *

11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | *

16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup.journal; 20 | 21 | import android.app.AlertDialog; 22 | import android.app.ListActivity; 23 | import android.content.DialogInterface; 24 | import android.content.SharedPreferences; 25 | import android.databinding.DataBindingUtil; 26 | import android.os.Bundle; 27 | import android.preference.PreferenceManager; 28 | import android.view.View; 29 | import android.widget.AdapterView; 30 | import android.widget.ListView; 31 | import android.widget.Toast; 32 | import android.widget.ToggleButton; 33 | 34 | import fr.s13d.photobackup.Log; 35 | import fr.s13d.photobackup.PBApplication; 36 | import fr.s13d.photobackup.R; 37 | import fr.s13d.photobackup.databinding.ActivityJournalBinding; 38 | import fr.s13d.photobackup.interfaces.PBMediaSenderInterface; 39 | import fr.s13d.photobackup.media.PBMedia; 40 | import fr.s13d.photobackup.media.PBMediaSender; 41 | 42 | 43 | public class PBJournalActivity extends ListActivity implements PBMediaSenderInterface { 44 | 45 | private static final String LOG_TAG = "PBJournalActivity"; 46 | 47 | private PBJournalAdapter adapter; 48 | private PBMediaSender mediaSender; 49 | private SharedPreferences preferences; 50 | private ActivityJournalBinding binding; 51 | 52 | 53 | private final AdapterView.OnItemClickListener itemClickListener = new AdapterView.OnItemClickListener() { 54 | private PBMediaSender getMediaSender() { 55 | if (mediaSender == null) { 56 | mediaSender = new PBMediaSender(); 57 | mediaSender.addInterface(PBJournalActivity.this); 58 | } 59 | return mediaSender; 60 | } 61 | 62 | 63 | @Override 64 | public void onItemClick(AdapterView parent, View view, int position, long id) { 65 | try { 66 | final PBMedia media = adapter.getFilteredMedias().get(position); 67 | final AlertDialog.Builder builder = new AlertDialog.Builder(PBJournalActivity.this); 68 | builder.setMessage(PBJournalActivity.this.getResources().getString(R.string.manual_backup_message)) 69 | .setTitle(PBJournalActivity.this.getResources().getString(R.string.manual_backup_title)); 70 | builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { 71 | public void onClick(DialogInterface dialog, int id) { 72 | getMediaSender().send(media, true); 73 | } 74 | }); 75 | builder.setNegativeButton(PBJournalActivity.this.getString(R.string.cancel), null); 76 | builder.create().show(); 77 | } catch (NullPointerException e) { 78 | Log.e(LOG_TAG, e); 79 | } 80 | } 81 | }; 82 | 83 | 84 | @Override 85 | protected void onCreate(final Bundle savedInstanceState) { 86 | super.onCreate(savedInstanceState); 87 | 88 | // Set up the UI (with binding) 89 | binding = DataBindingUtil.setContentView(this, R.layout.activity_journal); 90 | 91 | // layout 92 | setContentView(R.layout.activity_journal); 93 | 94 | // preferences 95 | initPreferences(); 96 | 97 | // on click listener 98 | final ListView list = (ListView) findViewById(android.R.id.list); 99 | list.setOnItemClickListener(itemClickListener); 100 | 101 | // adapter 102 | adapter = new PBJournalAdapter(this, PBApplication.getMediaStore().getMediaList()); 103 | setListAdapter(adapter); 104 | adapter.getFilter().filter(null); // to init the view 105 | } 106 | 107 | 108 | @Override 109 | protected void onDestroy() { 110 | super.onDestroy(); 111 | adapter.close(); 112 | } 113 | 114 | 115 | @Override 116 | protected void onResume() { 117 | super.onResume(); 118 | adapter.notifyDataSetChanged(); 119 | } 120 | 121 | ///////////////////// 122 | // private methods // 123 | ///////////////////// 124 | private void initPreferences() { 125 | if (preferences == null) { 126 | preferences = PreferenceManager.getDefaultSharedPreferences(this); 127 | } 128 | 129 | // set stored values 130 | binding.savedToggleButton.setChecked(preferences.getBoolean(PBMedia.PBMediaState.SYNCED.name(), true)); 131 | binding.waitingToggleButton.setChecked(preferences.getBoolean(PBMedia.PBMediaState.WAITING.name(), true)); 132 | binding.errorToggleButton.setChecked(preferences.getBoolean(PBMedia.PBMediaState.ERROR.name(), true)); 133 | } 134 | 135 | 136 | ////////////////// 137 | // buttons call // 138 | ////////////////// 139 | public void clickOnSaved(View v) { 140 | Log.i(LOG_TAG, "clickOnSaved"); 141 | ToggleButton btn = (ToggleButton) v; 142 | final SharedPreferences.Editor preferencesEditor = preferences.edit(); 143 | preferencesEditor.putBoolean(PBMedia.PBMediaState.SYNCED.name(), btn.isChecked()).apply(); 144 | adapter.getFilter().filter(null); 145 | } 146 | 147 | 148 | public void clickOnWaiting(View v) { 149 | Log.i(LOG_TAG, "clickOnWaiting"); 150 | ToggleButton btn = (ToggleButton) v; 151 | final SharedPreferences.Editor preferencesEditor = preferences.edit(); 152 | preferencesEditor.putBoolean(PBMedia.PBMediaState.WAITING.name(), btn.isChecked()).apply(); 153 | adapter.getFilter().filter(null); 154 | } 155 | 156 | 157 | public void clickOnError(View v) { 158 | Log.i(LOG_TAG, "clickOnError"); 159 | ToggleButton btn = (ToggleButton) v; 160 | final SharedPreferences.Editor preferencesEditor = preferences.edit(); 161 | preferencesEditor.putBoolean(PBMedia.PBMediaState.ERROR.name(), btn.isChecked()).apply(); 162 | adapter.getFilter().filter(null); 163 | } 164 | 165 | 166 | //////////////////////////// 167 | // PBMediaSenderInterface // 168 | //////////////////////////// 169 | public void onMessage(final String message) { 170 | runOnUiThread(new Runnable() { 171 | @Override 172 | public void run() { 173 | Toast.makeText(PBApplication.getApp(), message, Toast.LENGTH_SHORT).show(); 174 | } 175 | }); 176 | } 177 | 178 | 179 | @Override 180 | public void onSendSuccess() { 181 | runOnUiThread(new Runnable() { 182 | @Override 183 | public void run() { 184 | Log.i(LOG_TAG, "Trying to refresh view"); 185 | adapter.notifyDataSetChanged(); 186 | } 187 | }); 188 | } 189 | 190 | 191 | @Override 192 | public void onSendFailure() { 193 | onSendSuccess(); 194 | } 195 | 196 | 197 | @Override 198 | public void onTestSuccess() { 199 | // Do nothing 200 | } 201 | 202 | 203 | @Override 204 | public void onTestFailure() { 205 | // Do nothing 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/journal/PBJournalAdapter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | *

4 | * This file is part of PhotoBackup. 5 | *

6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | *

11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | *

16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup.journal; 20 | 21 | import android.app.Activity; 22 | import android.content.Context; 23 | import android.content.SharedPreferences; 24 | import android.graphics.Bitmap; 25 | import android.os.Handler; 26 | import android.os.HandlerThread; 27 | import android.os.Message; 28 | import android.preference.PreferenceManager; 29 | import android.provider.MediaStore.Video; 30 | import android.provider.MediaStore.Images; 31 | import android.support.annotation.NonNull; 32 | import android.view.LayoutInflater; 33 | import android.view.View; 34 | import android.view.ViewGroup; 35 | import android.widget.ArrayAdapter; 36 | import android.widget.Filter; 37 | import android.widget.Filterable; 38 | import android.widget.ImageView; 39 | import android.widget.TextView; 40 | 41 | import java.io.File; 42 | import java.lang.ref.WeakReference; 43 | import java.util.ArrayList; 44 | import java.util.List; 45 | 46 | import fr.s13d.photobackup.Log; 47 | import fr.s13d.photobackup.R; 48 | import fr.s13d.photobackup.media.PBMedia; 49 | 50 | 51 | class PBJournalAdapter extends ArrayAdapter implements Filterable, Handler.Callback { 52 | private static final String LOG_TAG = "PBJournalAdapter"; 53 | private static LayoutInflater inflater; 54 | private final Context context; 55 | private final SharedPreferences preferences; 56 | private final WeakReference handlerWeakReference; 57 | private Filter filter; 58 | private final List medias; 59 | private List filteredMedias; 60 | 61 | 62 | PBJournalAdapter(final Activity activity, final List medias) { 63 | super(activity, 0, medias); 64 | 65 | this.context = activity; 66 | this.preferences = PreferenceManager.getDefaultSharedPreferences(activity); 67 | this.medias = medias; 68 | this.filteredMedias = new ArrayList<>(medias.size()); 69 | 70 | inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 71 | 72 | // Create and start a new thread for the handler 73 | final HandlerThread handlerThread = new HandlerThread("BackgroundThread"); 74 | handlerThread.start(); 75 | handlerWeakReference = new WeakReference<>(new Handler(handlerThread.getLooper(), this)); 76 | } 77 | 78 | 79 | void close() { 80 | if (handlerWeakReference != null) { 81 | final Handler handler = handlerWeakReference.get(); 82 | if (handler != null) { 83 | handler.getLooper().quit(); 84 | } 85 | } 86 | } 87 | 88 | 89 | ////////////////////// 90 | // Handler.Callback // 91 | ////////////////////// 92 | @Override 93 | public boolean handleMessage(Message message) { 94 | // done on async thread 95 | final View view = (View) message.obj; 96 | final ImageView thumbImageView = (ImageView) view.findViewById(R.id.thumbnail); 97 | Bitmap bitmap = Images.Thumbnails.getThumbnail(context.getContentResolver(), 98 | message.what, Images.Thumbnails.MICRO_KIND, null); 99 | if (bitmap == null) { 100 | bitmap = Video.Thumbnails.getThumbnail(context.getContentResolver(), 101 | message.what, Video.Thumbnails.MICRO_KIND, null); 102 | } 103 | final Bitmap fBitmap = bitmap; 104 | 105 | // back on UI thread to set the bitmap to the view 106 | new Handler(context.getMainLooper()).post(new Runnable() { 107 | @Override 108 | public void run() { 109 | thumbImageView.setImageBitmap(fBitmap); 110 | } 111 | }); 112 | 113 | return true; 114 | } 115 | 116 | 117 | ///////////// 118 | // Adapter // 119 | ///////////// 120 | @NonNull 121 | @Override 122 | public View getView(int position, View adapterView, @NonNull ViewGroup parent) { 123 | // create the view if not available 124 | final View view = (adapterView == null) ? inflater.inflate(R.layout.list_row, parent, false) : adapterView; 125 | 126 | final PBMedia media = filteredMedias.get(position); 127 | if (media == null || media.getId() == -1) { 128 | return view; 129 | } 130 | 131 | // create thumbnail 132 | if (handlerWeakReference != null) { 133 | final Handler handler = handlerWeakReference.get(); 134 | if (handler != null) { 135 | handler.obtainMessage(media.getId(), view).sendToTarget(); 136 | } 137 | } 138 | 139 | // filename 140 | final TextView textView = (TextView) view.findViewById(R.id.filename); 141 | if (media.getPath() != null) { 142 | final File file = new File(media.getPath()); 143 | textView.setText(file.getName()); 144 | } else { 145 | textView.setText(R.string.journal_error); 146 | } 147 | 148 | // indicator 149 | setIndicator(view, media); 150 | 151 | return view; 152 | } 153 | 154 | 155 | private void setIndicator(final View view, final PBMedia media) { 156 | final TextView errorTextView = (TextView) view.findViewById(R.id.errorHint); 157 | final ImageView imageView = (ImageView) view.findViewById(R.id.state); 158 | if (media.getState() == PBMedia.PBMediaState.WAITING) { 159 | errorTextView.setVisibility(View.GONE); 160 | imageView.setImageResource(android.R.drawable.presence_away); 161 | } else if (media.getState() == PBMedia.PBMediaState.SYNCED) { 162 | errorTextView.setVisibility(View.GONE); 163 | imageView.setImageResource(android.R.drawable.presence_online); 164 | } else if (media.getState() == PBMedia.PBMediaState.ERROR) { 165 | errorTextView.setText(media.getErrorMessage()); 166 | imageView.setImageResource(android.R.drawable.presence_busy); 167 | } 168 | 169 | } 170 | 171 | 172 | @Override 173 | public int getCount() { 174 | int count = 0; 175 | try { 176 | count = filteredMedias.size(); 177 | } catch (NullPointerException e) { 178 | Log.w(LOG_TAG, "count = " + count); 179 | Log.w(LOG_TAG, e); 180 | } 181 | return count; 182 | } 183 | 184 | 185 | @Override 186 | public PBMedia getItem(final int position) { 187 | PBMedia item = null; 188 | try { 189 | item = filteredMedias.get(position); 190 | } catch (NullPointerException e) { 191 | Log.w(LOG_TAG, e); 192 | } 193 | return item; 194 | } 195 | 196 | 197 | @Override 198 | public long getItemId(final int position) { 199 | return position; 200 | } 201 | 202 | 203 | //////////////// 204 | // Filterable // 205 | //////////////// 206 | @NonNull 207 | @Override 208 | public Filter getFilter() { 209 | if (filter == null) { 210 | filter = new Filter() { 211 | 212 | @SuppressWarnings("unchecked") 213 | @Override 214 | protected void publishResults(CharSequence constraint, FilterResults results) { 215 | 216 | filteredMedias = (List) results.values; 217 | notifyDataSetChanged(); 218 | } 219 | 220 | @Override 221 | protected FilterResults performFiltering(CharSequence constraint) { 222 | 223 | final FilterResults results = new FilterResults(); 224 | final List mediaList = getMediaList(); 225 | results.values = mediaList; 226 | results.count = mediaList.size(); 227 | return results; 228 | } 229 | }; 230 | } 231 | return filter; 232 | } 233 | 234 | 235 | ///////////////////// 236 | // Private methods // 237 | ///////////////////// 238 | private List getMediaList() { 239 | 240 | filteredMedias.clear(); 241 | final Boolean synced = preferences.getBoolean(PBMedia.PBMediaState.SYNCED.name(), true); 242 | final Boolean waiting = preferences.getBoolean(PBMedia.PBMediaState.WAITING.name(), true); 243 | final Boolean error = preferences.getBoolean(PBMedia.PBMediaState.ERROR.name(), true); 244 | 245 | for (int i = 0; i < medias.size(); i++) { 246 | final PBMedia media = medias.get(i); 247 | final boolean stateSynced = media.getState() == PBMedia.PBMediaState.SYNCED && synced; 248 | final boolean stateWaiting = media.getState() == PBMedia.PBMediaState.WAITING && waiting; 249 | final boolean stateError = media.getState() == PBMedia.PBMediaState.ERROR && error; 250 | if (stateSynced || stateWaiting || stateError) { 251 | filteredMedias.add(media); 252 | } 253 | } 254 | return filteredMedias; 255 | } 256 | 257 | 258 | // Getter // 259 | List getFilteredMedias() { 260 | return filteredMedias; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/media/PBMedia.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup.media; 20 | 21 | import android.content.Context; 22 | import android.content.SharedPreferences; 23 | import android.database.Cursor; 24 | 25 | import java.io.Serializable; 26 | 27 | import fr.s13d.photobackup.Log; 28 | import fr.s13d.photobackup.PBApplication; 29 | 30 | 31 | /** 32 | * Media object adding information over device medias. 33 | */ 34 | public class PBMedia implements Serializable { 35 | private final int id; 36 | private final String path; 37 | private final long dateAdded; 38 | private final SharedPreferences mediasPreferences; 39 | private String errorMessage; 40 | private PBMediaState state; 41 | 42 | /** 43 | * Enumeration of all possible media states 44 | * - waiting: the media waits for being uploaded; 45 | * - synced: the media is synced with the server already; 46 | * - error: an error occurred. 47 | */ 48 | public enum PBMediaState { WAITING, SYNCED, ERROR } 49 | 50 | 51 | ////////////////// 52 | // Constructors // 53 | ////////////////// 54 | PBMedia(Cursor mediaCursor) { 55 | this.id = mediaCursor.getInt(mediaCursor.getColumnIndexOrThrow("_id")); 56 | this.path = mediaCursor.getString(mediaCursor.getColumnIndexOrThrow("_data")); 57 | this.dateAdded = mediaCursor.getLong(mediaCursor.getColumnIndexOrThrow("date_added")); 58 | this.mediasPreferences = PBApplication.getApp().getSharedPreferences(PBApplication.PB_MEDIAS_SHARED_PREFS, Context.MODE_PRIVATE); 59 | 60 | // Find state from the shared preferences 61 | String stateString = mediasPreferences.getString(String.valueOf(this.id), PBMedia.PBMediaState.WAITING.name()); 62 | this.state = PBMedia.PBMediaState.valueOf(stateString); 63 | this.errorMessage = ""; 64 | } 65 | 66 | 67 | //////////// 68 | // Methods// 69 | //////////// 70 | 71 | // Will be used by the ArrayAdapter in the ListView 72 | @Override 73 | public String toString() { 74 | return "PBMedia: " + this.path; 75 | } 76 | 77 | 78 | ///////////////////////////////////////// 79 | // Getters/Setters are the Java fun... // 80 | ///////////////////////////////////////// 81 | public int getId() { 82 | return this.id; 83 | } 84 | 85 | public String getPath() { 86 | return this.path; 87 | } 88 | 89 | long getDateAdded() { 90 | return this.dateAdded; 91 | } 92 | 93 | public long getAge() { 94 | return System.currentTimeMillis() / 1000 - this.dateAdded; 95 | } 96 | 97 | public String getErrorMessage() { return this.errorMessage; } 98 | 99 | void setErrorMessage(String newMessage) { this.errorMessage = newMessage; } 100 | 101 | public PBMediaState getState() { 102 | return this.state; 103 | } 104 | 105 | public void setState(PBMediaState mediaState) { 106 | if (this.state != mediaState) { 107 | this.state = mediaState; 108 | mediasPreferences.edit().putString(String.valueOf(this.getId()), mediaState.name()).apply(); 109 | Log.i("PBMedia", "Set state " + mediaState.toString() + " to " + this.getPath()); 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/media/PBMediaSender.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup.media; 20 | 21 | import android.app.Notification; 22 | import android.app.NotificationManager; 23 | import android.app.PendingIntent; 24 | import android.content.Context; 25 | import android.content.Intent; 26 | import android.content.SharedPreferences; 27 | import android.graphics.Bitmap; 28 | import android.graphics.BitmapFactory; 29 | import android.net.ConnectivityManager; 30 | import android.net.NetworkInfo; 31 | import android.preference.PreferenceManager; 32 | 33 | import java.io.File; 34 | import java.io.IOException; 35 | import java.util.ArrayList; 36 | import java.util.List; 37 | import java.util.concurrent.TimeUnit; 38 | 39 | import fr.s13d.photobackup.Log; 40 | import fr.s13d.photobackup.PBActivity; 41 | import fr.s13d.photobackup.PBApplication; 42 | import fr.s13d.photobackup.PBConstants; 43 | import fr.s13d.photobackup.R; 44 | import fr.s13d.photobackup.interfaces.PBMediaSenderInterface; 45 | import fr.s13d.photobackup.preferences.PBServerPreferenceFragment; 46 | import okhttp3.Call; 47 | import okhttp3.Callback; 48 | import okhttp3.Credentials; 49 | import okhttp3.FormBody; 50 | import okhttp3.MediaType; 51 | import okhttp3.MultipartBody; 52 | import okhttp3.OkHttpClient; 53 | import okhttp3.Request; 54 | import okhttp3.RequestBody; 55 | import okhttp3.Response; 56 | 57 | 58 | /** 59 | * The sender of medias 60 | */ 61 | public class PBMediaSender { 62 | private static final String LOG_TAG = "PBMediaSender"; 63 | private static final String PASS_PARAM = "password"; 64 | private static final String UPFILE_PARAM = "upfile"; 65 | private static final String FILESIZE_PARAM = "filesize"; 66 | private static final String TEST_PATH = "/test"; 67 | private String serverUrl; 68 | private final SharedPreferences preferences; 69 | private final NotificationManager notificationManager; 70 | private String credentials; 71 | private Notification.Builder builder; 72 | private OkHttpClient okClient; 73 | private static final List interfaces = new ArrayList<>(); 74 | private static int failureCount = 0; 75 | private static final int TIMEOUT_IN_SECONDS = 60; 76 | 77 | 78 | public PBMediaSender() { 79 | notificationManager = (NotificationManager) PBApplication.getApp().getSystemService(Context.NOTIFICATION_SERVICE); 80 | preferences = PreferenceManager.getDefaultSharedPreferences(PBApplication.getApp()); 81 | serverUrl = removeFinalSlashes(preferences.getString(PBServerPreferenceFragment.PREF_SERVER_URL, "")); 82 | } 83 | 84 | 85 | public void addInterface(PBMediaSenderInterface senderInterface) { 86 | interfaces.add(senderInterface); 87 | } 88 | 89 | 90 | //////////////// 91 | // Send media // 92 | //////////////// 93 | public void send(final PBMedia media, boolean manual) { 94 | // network 95 | final String wifiOnlyString = preferences.getString(PBConstants.PREF_WIFI_ONLY, PBApplication.getApp().getResources().getString(R.string.only_wifi_default)); 96 | final boolean wifiOnly = wifiOnlyString.equals(PBApplication.getApp().getResources().getString(R.string.only_wifi)); 97 | final ConnectivityManager cm = (ConnectivityManager) PBApplication.getApp().getSystemService(Context.CONNECTIVITY_SERVICE); 98 | final NetworkInfo info = cm.getActiveNetworkInfo(); 99 | final boolean onWifi = info != null && info.isConnected() && info.getType() == ConnectivityManager.TYPE_WIFI; 100 | 101 | // recently taken picture 102 | final String uploadRecentOnlyString = preferences.getString(PBConstants.PREF_RECENT_UPLOAD_ONLY, PBApplication.getApp().getResources().getString(R.string.only_recent_upload_default)); 103 | final Boolean uploadRecentOnly = uploadRecentOnlyString.equals(PBApplication.getApp().getResources().getString(R.string.only_recent_upload)); 104 | final Boolean recentPicture = (System.currentTimeMillis() / 1000 - media.getDateAdded()) < 600; 105 | 106 | Log.i(LOG_TAG, "Connectivity: onWifi=" + onWifi + ", wifiOnly=" + wifiOnly + ", recentPicture=" + recentPicture.toString()); 107 | // test to send or not 108 | final boolean conditionToSend = (!wifiOnly || onWifi) && (!uploadRecentOnly || recentPicture); 109 | if (manual || conditionToSend) { 110 | sendMedia(media); 111 | } 112 | } 113 | 114 | 115 | private void sendMedia(final PBMedia media) { 116 | final MediaType mediaTypeJPG = MediaType.parse("image/jpg"); 117 | final File upfile = new File(media.getPath()); 118 | final RequestBody requestBody = new MultipartBody.Builder() 119 | .setType(MultipartBody.FORM) 120 | .addFormDataPart(PASS_PARAM, preferences.getString(PBServerPreferenceFragment.PREF_SERVER_PASS_HASH, "")) 121 | .addFormDataPart(FILESIZE_PARAM, String.valueOf(upfile.length())) 122 | .addFormDataPart(UPFILE_PARAM, upfile.getName(), RequestBody.create(mediaTypeJPG, upfile)) 123 | .build(); 124 | final Request request = makePostRequest(requestBody); 125 | getOkClient().newCall(request).enqueue(new Callback() { 126 | @Override 127 | public void onResponse(Call call, Response response) throws IOException { 128 | Log.i(LOG_TAG, "Get response with code " + response.code()); 129 | if (response.code() == 200 || response.code() == 409) { 130 | sendDidSucceed(media); 131 | } else { 132 | sendDidFail(media, new Throwable(response.message())); 133 | } 134 | response.body().close(); 135 | } 136 | 137 | @Override 138 | public void onFailure(Call call, IOException e) { 139 | Log.w(LOG_TAG, e.toString()); 140 | sendDidFail(media, e); 141 | } 142 | }); 143 | } 144 | 145 | 146 | /////////////// 147 | // Send test // 148 | /////////////// 149 | public void test() { 150 | final RequestBody requestBody = new FormBody.Builder() 151 | .add(PASS_PARAM, preferences.getString(PBServerPreferenceFragment.PREF_SERVER_PASS_HASH, "")) 152 | .build(); 153 | final Request request = makePostRequest(requestBody, TEST_PATH); 154 | Log.i(LOG_TAG, "Initiating test call to " + request.url()); 155 | 156 | getOkClient().newCall(request).enqueue(new Callback() { 157 | @Override 158 | public void onResponse(Call call, Response response) throws IOException { 159 | if (response.isSuccessful()) { 160 | testDidSucceed(); 161 | } else if (response.isRedirect()) { 162 | prepareRedirectFollowing(response); 163 | test(); // rerun 164 | } else { 165 | testDidFail(response.message()); 166 | } 167 | response.body().close(); 168 | } 169 | 170 | @Override 171 | public void onFailure(Call call, IOException e) { 172 | testDidFail(e.getLocalizedMessage()); 173 | Log.e(LOG_TAG, e); 174 | } 175 | }); 176 | } 177 | 178 | 179 | ///////////////////// 180 | // Private methods // 181 | ///////////////////// 182 | private Request makePostRequest(RequestBody requestBody) { 183 | return makePostRequest(requestBody, ""); 184 | } 185 | 186 | 187 | private Request makePostRequest(RequestBody requestBody, String pathSuffix) { 188 | final Request.Builder requestBuilder = new Request.Builder() 189 | .url(serverUrl + pathSuffix) 190 | .header("User-Agent", PBApplication.PB_USER_AGENT) 191 | .post(requestBody); 192 | if (credentials != null) { 193 | requestBuilder.header("Authorization", credentials); 194 | } 195 | return requestBuilder.build(); 196 | } 197 | 198 | 199 | private void prepareRedirectFollowing(final Response response) { 200 | final String redirection = response.header("Location"); 201 | Log.d(LOG_TAG, "Redirected to " + redirection); 202 | String newUrl = removeFinalSlashes(redirection).substring(0, redirection.length() - TEST_PATH.length()); 203 | Log.d(LOG_TAG, "Update server url to " + newUrl); 204 | serverUrl = newUrl; 205 | preferences.edit().putString(PBServerPreferenceFragment.PREF_SERVER_URL, newUrl).apply(); 206 | } 207 | 208 | 209 | private void createAuthCredentials() { 210 | // add HTTP Basic Auth to the client 211 | final String login = preferences.getString(PBServerPreferenceFragment.PREF_SERVER_HTTP_AUTH_LOGIN, ""); 212 | final String pass = preferences.getString(PBServerPreferenceFragment.PREF_SERVER_HTTP_AUTH_PASS, ""); 213 | if (preferences.getBoolean(PBServerPreferenceFragment.PREF_SERVER_HTTP_AUTH_SWITCH, false) && 214 | !preferences.getString(PBServerPreferenceFragment.PREF_SERVER_HTTP_AUTH_LOGIN, "").isEmpty() && 215 | !preferences.getString(PBServerPreferenceFragment.PREF_SERVER_HTTP_AUTH_PASS, "").isEmpty()) { 216 | credentials = Credentials.basic(login, pass); 217 | } else { 218 | credentials = null; 219 | } 220 | } 221 | 222 | 223 | private void sendDidSucceed(final PBMedia media) { 224 | media.setState(PBMedia.PBMediaState.SYNCED); 225 | media.setErrorMessage(""); 226 | for (PBMediaSenderInterface senderInterface : interfaces) { 227 | senderInterface.onSendSuccess(); 228 | } 229 | } 230 | 231 | 232 | private void sendDidFail(final PBMedia media, final Throwable e) { 233 | getNotificationBuilder().setSmallIcon(R.drawable.ic_error_outline_white_48dp); 234 | media.setState(PBMedia.PBMediaState.ERROR); 235 | media.setErrorMessage(e.getLocalizedMessage()); 236 | for (PBMediaSenderInterface senderInterface : interfaces) { 237 | senderInterface.onSendFailure(); 238 | } 239 | Log.w(LOG_TAG, e.toString()); 240 | incrementFailureCount(); 241 | updateNotificationText(); 242 | } 243 | 244 | 245 | private void testDidSucceed() { 246 | for (final PBMediaSenderInterface senderInterface : interfaces) { 247 | senderInterface.onMessage(PBApplication.getApp().getResources().getString(R.string.toast_configuration_ok)); 248 | senderInterface.onTestSuccess(); 249 | } 250 | } 251 | 252 | 253 | private void testDidFail(final String failMessage) { 254 | final String message = PBApplication.getApp().getResources().getString(R.string.toast_configuration_ko) + " - (" + failMessage + ")"; 255 | Log.d(LOG_TAG, message); 256 | for (final PBMediaSenderInterface senderInterface : interfaces) { 257 | senderInterface.onMessage(message); 258 | senderInterface.onTestFailure(); 259 | } 260 | } 261 | 262 | 263 | private void updateNotificationText() { 264 | final String failureContent = PBApplication.getApp().getResources().getQuantityString(R.plurals.notif_failure, failureCount, failureCount); 265 | getNotificationBuilder().setContentText(failureContent); 266 | notificationManager.notify(0, getNotificationBuilder().build()); 267 | } 268 | 269 | 270 | ////////////////// 271 | // Lazy loaders // 272 | ////////////////// 273 | private OkHttpClient getOkClient() { 274 | if (okClient == null) { 275 | okClient = new OkHttpClient.Builder() 276 | .readTimeout(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS) 277 | .connectTimeout(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS) 278 | .writeTimeout(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS) 279 | .build(); 280 | createAuthCredentials(); 281 | } 282 | return okClient; 283 | } 284 | 285 | 286 | private Notification.Builder getNotificationBuilder() { 287 | if (builder == null) { 288 | final Bitmap icon = BitmapFactory.decodeResource(PBApplication.getApp().getResources(), R.mipmap.ic_launcher); 289 | builder = new Notification.Builder(PBApplication.getApp()); 290 | builder.setSmallIcon(R.drawable.ic_backup_white_48dp) 291 | .setContentTitle(PBApplication.getApp().getResources().getString(R.string.app_name)) 292 | .setLargeIcon(icon); 293 | 294 | // add content intent to reopen the activity 295 | final Intent intent = new Intent(PBApplication.getApp(), PBActivity.class); 296 | intent.setAction(Intent.ACTION_MAIN); 297 | intent.addCategory(Intent.CATEGORY_LAUNCHER); 298 | final PendingIntent resultPendingIntent = PendingIntent.getActivity(PBApplication.getApp(), 0, intent, 0); 299 | builder.setContentIntent(resultPendingIntent); 300 | } 301 | return builder; 302 | } 303 | 304 | 305 | /////////// 306 | // Utils // 307 | /////////// 308 | private static String removeFinalSlashes(String s) { 309 | if (s == null || s.length() == 0) { 310 | return s; 311 | } 312 | int count = 0; 313 | int length = s.length(); 314 | while (s.charAt(length - 1 - count) == '/') { 315 | count++; 316 | } 317 | 318 | return s.substring(0, s.length() - count); 319 | } 320 | 321 | 322 | private static void incrementFailureCount() { 323 | failureCount++; 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/media/PBMediaStore.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup.media; 20 | 21 | import android.content.ContentResolver; 22 | import android.content.Context; 23 | import android.content.SharedPreferences; 24 | import android.database.Cursor; 25 | import android.database.MergeCursor; 26 | import android.net.Uri; 27 | import android.preference.PreferenceManager; 28 | import android.provider.MediaStore; 29 | import android.support.v4.util.ArrayMap; 30 | import android.text.TextUtils; 31 | 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | import java.util.Set; 35 | 36 | import fr.s13d.photobackup.Log; 37 | import fr.s13d.photobackup.PBApplication; 38 | import fr.s13d.photobackup.PBConstants; 39 | import fr.s13d.photobackup.interfaces.PBMediaStoreInterface; 40 | 41 | import static android.preference.PreferenceManager.getDefaultSharedPreferences; 42 | 43 | 44 | public class PBMediaStore { 45 | 46 | private static final String LOG_TAG = "PBMediaStore"; 47 | private static List mediaList; 48 | private static PBSyncMediaStoreTask syncTask; 49 | private static final SharedPreferences picturesPreferences = PBApplication.getApp() 50 | .getSharedPreferences(PBApplication.PB_MEDIAS_SHARED_PREFS, Context.MODE_PRIVATE); 51 | private final List interfaces = new ArrayList<>(); 52 | private static final Uri imagesUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 53 | private static final Uri videosUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 54 | private static final String DATE_ADDED_DESC = "date_added DESC"; 55 | private static final String[] imagesProjection = new String[] { "_id", "_data", "date_added" }; 56 | 57 | 58 | //////////////// 59 | // Life-cycle // 60 | //////////////// 61 | public void addInterface(PBMediaStoreInterface storeInterface) { 62 | interfaces.add(storeInterface); 63 | } 64 | 65 | 66 | public void removeInterface(PBMediaStoreInterface storeInterface) { 67 | interfaces.remove(storeInterface); 68 | } 69 | 70 | 71 | public void close() { 72 | if (syncTask != null) { 73 | syncTask.cancel(true); 74 | } 75 | setMediaListToNull(); 76 | } 77 | 78 | 79 | ///////////// 80 | // Queries // 81 | ///////////// 82 | public PBMedia createMediaForLatestInStore(boolean backupVideos) { 83 | final ContentResolver cr = PBApplication.getApp().getContentResolver(); 84 | final Cursor cursor = cr.query(backupVideos ? videosUri : imagesUri, null, null, null, DATE_ADDED_DESC); 85 | if (cursor == null || !cursor.moveToFirst()) { 86 | Log.d(LOG_TAG, "Media cursor is null or empty."); 87 | closeCursor(cursor); 88 | return null; 89 | } 90 | 91 | final int bucketId = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID); 92 | if (!backupVideos && !isBucketSelected(cursor.getString(bucketId))) { 93 | Log.d(LOG_TAG, "Media not in selected buckets."); 94 | closeCursor(cursor); 95 | return null; 96 | } 97 | 98 | final PBMedia media = new PBMedia(cursor); 99 | try { 100 | String stateString = picturesPreferences.getString(String.valueOf(media.getId()), PBMedia.PBMediaState.WAITING.name()); 101 | media.setState(PBMedia.PBMediaState.valueOf(stateString)); 102 | } catch (Exception e) { 103 | Log.e(LOG_TAG, e); 104 | } 105 | closeCursor(cursor); 106 | 107 | return media; 108 | } 109 | 110 | 111 | Cursor getAllMediasCursor() { 112 | String where = null; 113 | final SharedPreferences prefs = getDefaultSharedPreferences(PBApplication.getApp()); 114 | final Set bucketIds = prefs.getStringSet(PBConstants.PREF_PICTURE_FOLDER_LIST, null); 115 | if (bucketIds != null && !bucketIds.isEmpty()) { 116 | final String bucketString = TextUtils.join(", ", bucketIds); 117 | where = "bucket_id in (" + bucketString + ")"; 118 | } 119 | 120 | final boolean backupVideos = prefs.getBoolean(PBConstants.PREF_MEDIA_BACKUP_VIDEO, false); 121 | final ContentResolver cr = PBApplication.getApp().getContentResolver(); 122 | final Cursor[] cursors = new Cursor[backupVideos ? 2 : 1]; 123 | cursors[0] = cr.query(imagesUri, imagesProjection, where, null, DATE_ADDED_DESC); 124 | if (backupVideos) { 125 | cursors[1] = cr.query(videosUri, imagesProjection, where, null, DATE_ADDED_DESC); 126 | } 127 | if (cursors[0] == null) { 128 | Log.d(LOG_TAG, "Media cursor is null."); 129 | return null; 130 | } 131 | 132 | return new MergeCursor(cursors); 133 | } 134 | 135 | 136 | private boolean isBucketSelected(final String requestedBucketId) { 137 | Log.d(LOG_TAG, "Checking if bucket " + requestedBucketId + " is selected by user."); 138 | final SharedPreferences prefs = getDefaultSharedPreferences(PBApplication.getApp()); 139 | final Set bucketSet = prefs.getStringSet(PBConstants.PREF_PICTURE_FOLDER_LIST, null); 140 | return bucketSet != null && bucketSet.contains(requestedBucketId); 141 | } 142 | 143 | 144 | private void getBucketData(final ArrayMap bucketNamesList, final String buckedId, final String buckedName) { 145 | 146 | // We want to group the images by bucket names. We abuse the 147 | // "WHERE" parameter to insert a "GROUP BY" clause into the SQL statement. 148 | // The template for "WHERE" parameter is like: 149 | // SELECT ... FROM ... WHERE (%s) 150 | // and we make it look like: 151 | // SELECT ... FROM ... WHERE (1) GROUP BY (2) 152 | // The "(1)" means true. The "(2)" means the second columns specified in projection. 153 | // Note that because there is a ")" in the template, we use "(2" to match it. 154 | final String[] projection = { buckedId, buckedName, "count(*) as media_count" }; 155 | final String groupBy = "1) GROUP BY (2"; 156 | final Cursor cursor = PBApplication.getApp().getContentResolver().query(imagesUri, projection, groupBy, null, "media_count desc"); 157 | 158 | if (cursor != null && cursor.moveToFirst()) { 159 | String name; 160 | String id; 161 | String count; 162 | final int bucketIdColumn = cursor.getColumnIndex(buckedId); 163 | final int bucketNameColumn = cursor.getColumnIndex(buckedName); 164 | final int bucketCountColumn = cursor.getColumnIndex("media_count"); 165 | do { 166 | id = cursor.getString(bucketIdColumn); 167 | name = cursor.getString(bucketNameColumn); 168 | count = cursor.getString(bucketCountColumn); 169 | bucketNamesList.put(id, name + " (" + count + ")"); 170 | } while (cursor.moveToNext()); 171 | } 172 | closeCursor(cursor); 173 | 174 | Log.d(LOG_TAG, bucketNamesList.toString()); 175 | } 176 | 177 | 178 | /** 179 | * @return list of device media bucket names 180 | */ 181 | public ArrayMap getBucketData() { 182 | final ArrayMap bucketNames = new ArrayMap<>(); 183 | getBucketData(bucketNames, MediaStore.Images.ImageColumns.BUCKET_ID, MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME); 184 | 185 | final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(PBApplication.getApp()); 186 | final boolean backupVideos = sp.getBoolean(PBConstants.PREF_MEDIA_BACKUP_VIDEO, false); 187 | if (backupVideos) { 188 | getBucketData(bucketNames, MediaStore.Video.VideoColumns.BUCKET_ID, MediaStore.Video.VideoColumns.BUCKET_DISPLAY_NAME); 189 | } 190 | return bucketNames; 191 | } 192 | 193 | 194 | private void closeCursor(Cursor cursor) { 195 | if (cursor != null && !cursor.isClosed()) { 196 | cursor.close(); 197 | } 198 | } 199 | 200 | 201 | ///////////////////////////////// 202 | // Synchronize the media store // 203 | ///////////////////////////////// 204 | public void sync() { 205 | if (syncTask != null) { 206 | syncTask.cancel(true); 207 | } 208 | 209 | setSyncTask(new PBSyncMediaStoreTask()); 210 | syncTask.execute(); 211 | Log.i(LOG_TAG, "Start SyncMediaStoreTask"); 212 | } 213 | 214 | 215 | void onPostSync() { 216 | for(PBMediaStoreInterface storeInterface : interfaces) { 217 | storeInterface.onSyncMediaStoreTaskPostExecute(); 218 | } 219 | Log.i(LOG_TAG, "Stop PBSyncMediaStoreTask"); 220 | } 221 | 222 | 223 | ////////////////// 224 | // Lazy loaders // 225 | ////////////////// 226 | public List getMediaList() { 227 | if (mediaList == null) { 228 | setMediaList(new ArrayList()); 229 | } 230 | return mediaList; 231 | } 232 | 233 | 234 | //////////////////// 235 | // Static setters // 236 | //////////////////// 237 | private static void setMediaListToNull(){ mediaList = null; } 238 | private static void setSyncTask(PBSyncMediaStoreTask syncTask) { PBMediaStore.syncTask = syncTask; } 239 | private static void setMediaList(List mediaList) { PBMediaStore.mediaList = mediaList; } 240 | } 241 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/media/PBSyncMediaStoreTask.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup.media; 20 | 21 | import android.content.Context; 22 | import android.content.SharedPreferences; 23 | import android.database.Cursor; 24 | import android.os.AsyncTask; 25 | 26 | import java.util.HashSet; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.Set; 30 | 31 | import fr.s13d.photobackup.Log; 32 | import fr.s13d.photobackup.PBApplication; 33 | 34 | 35 | class PBSyncMediaStoreTask extends AsyncTask { 36 | 37 | private static final String LOG_TAG = "PBSyncMediaStoreTask"; 38 | private final SharedPreferences picturesPreferences; 39 | 40 | 41 | ///////////////// 42 | // Constructor // 43 | ///////////////// 44 | PBSyncMediaStoreTask() { 45 | this.picturesPreferences = PBApplication.getApp().getSharedPreferences(PBApplication.PB_MEDIAS_SHARED_PREFS, Context.MODE_PRIVATE); 46 | } 47 | 48 | 49 | ///////////////////////////////// 50 | // What makes you an AsyncTask // 51 | ///////////////////////////////// 52 | protected Void doInBackground(Void... voids) { 53 | 54 | // Get all known pictures in PB 55 | final Map mediasMap = picturesPreferences.getAll(); 56 | final Set inCursor = new HashSet<>(); 57 | 58 | // Get all pictures on device 59 | Cursor cursor = null; 60 | try { 61 | cursor = PBApplication.getMediaStore().getAllMediasCursor(); 62 | } catch (SecurityException e) { 63 | Log.d(LOG_TAG, e); 64 | } 65 | 66 | // loop through them to sync 67 | PBMedia media; 68 | String stateString; 69 | PBMedia.PBMediaState state; 70 | final List mediaList = PBApplication.getMediaStore().getMediaList(); 71 | mediaList.clear(); 72 | while (cursor != null && cursor.moveToNext()) { 73 | if(isCancelled()) { 74 | Log.i(LOG_TAG, "PBSyncMediaStoreTask cancelled"); 75 | return null; 76 | } 77 | // create new media 78 | media = new PBMedia(cursor); 79 | stateString = (String)mediasMap.get(Integer.toString(media.getId())); 80 | state = (stateString != null) ? PBMedia.PBMediaState.valueOf(stateString) : PBMedia.PBMediaState.WAITING; 81 | media.setState(state); 82 | mediaList.add(media); // populate list 83 | inCursor.add(Integer.toString(media.getId())); 84 | } 85 | if (cursor != null && !cursor.isClosed()) { 86 | cursor.close(); 87 | } 88 | 89 | // purge pictures in preferences that were removed from device 90 | final Set inCursorCopy = new HashSet<>(inCursor); 91 | final Set inPreferences = new HashSet<>(mediasMap.keySet()); 92 | inPreferences.removeAll(inCursor); 93 | inCursor.removeAll(inCursorCopy); 94 | inPreferences.addAll(inCursor); 95 | 96 | for (final String key : inPreferences) { 97 | Log.d(LOG_TAG, "Remove media " + key + " from preference"); 98 | picturesPreferences.edit().remove(key).apply(); 99 | } 100 | 101 | return null; 102 | } 103 | 104 | protected void onPostExecute(Void result) { 105 | PBApplication.getMediaStore().onPostSync(); 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/preferences/PBPreferenceFragment.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package fr.s13d.photobackup.preferences; 21 | 22 | import android.Manifest; 23 | import android.app.Activity; 24 | import android.app.ActivityManager; 25 | import android.content.ComponentName; 26 | import android.content.Context; 27 | import android.content.Intent; 28 | import android.content.ServiceConnection; 29 | import android.content.SharedPreferences; 30 | import android.content.pm.PackageManager; 31 | import android.os.Build; 32 | import android.os.Bundle; 33 | import android.os.IBinder; 34 | import android.preference.ListPreference; 35 | import android.preference.MultiSelectListPreference; 36 | import android.preference.Preference; 37 | import android.preference.PreferenceFragment; 38 | import android.preference.PreferenceManager; 39 | import android.preference.PreferenceScreen; 40 | import android.preference.SwitchPreference; 41 | import android.support.v4.app.ActivityCompat; 42 | import android.support.v4.content.ContextCompat; 43 | import android.support.v4.util.ArrayMap; 44 | import android.text.TextUtils; 45 | import android.view.Menu; 46 | import android.view.MenuInflater; 47 | import android.view.MenuItem; 48 | import android.webkit.URLUtil; 49 | import android.widget.Toast; 50 | 51 | import java.util.ArrayList; 52 | import java.util.Arrays; 53 | import java.util.Map; 54 | import java.util.Set; 55 | 56 | import fr.s13d.photobackup.BuildConfig; 57 | import fr.s13d.photobackup.Log; 58 | import fr.s13d.photobackup.PBActivity; 59 | import fr.s13d.photobackup.PBApplication; 60 | import fr.s13d.photobackup.PBConstants; 61 | import fr.s13d.photobackup.media.PBMediaSender; 62 | import fr.s13d.photobackup.PBService; 63 | import fr.s13d.photobackup.R; 64 | import fr.s13d.photobackup.about.PBAboutActivity; 65 | import fr.s13d.photobackup.interfaces.PBMediaSenderInterface; 66 | import fr.s13d.photobackup.interfaces.PBMediaStoreInterface; 67 | import fr.s13d.photobackup.journal.PBJournalActivity; 68 | 69 | 70 | public class PBPreferenceFragment extends PreferenceFragment 71 | implements SharedPreferences.OnSharedPreferenceChangeListener, 72 | PBMediaStoreInterface, PBMediaSenderInterface { 73 | 74 | private static final String LOG_TAG = "PBPreferenceFragment"; 75 | private PBService currentService; 76 | private final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(PBApplication.getApp()); 77 | private ArrayMap bucketNames; 78 | public static final int PERMISSION_READ_EXTERNAL_STORAGE = 0; 79 | private int permissionOrigin; 80 | 81 | 82 | // binding 83 | private boolean isBoundToService = false; 84 | private final ServiceConnection serviceConnection = new ServiceConnection() { 85 | 86 | public void onServiceConnected(ComponentName className, IBinder binder) { 87 | PBService.Binder b = (PBService.Binder) binder; 88 | currentService = b.getService(); 89 | updatePreferences(); // update journal serverKeys number 90 | Log.i(LOG_TAG, "Connected to service"); 91 | } 92 | 93 | public void onServiceDisconnected(ComponentName className) { 94 | Log.i(LOG_TAG, "Disconnected to service"); 95 | } 96 | }; 97 | 98 | 99 | ////////////// 100 | // Override // 101 | ////////////// 102 | @Override 103 | public void onCreate(final Bundle savedInstanceState) { 104 | super.onCreate(savedInstanceState); 105 | migratePreferences(); 106 | 107 | // create preferences 108 | addPreferencesFromResource(R.xml.preferences); 109 | 110 | // add click listeners to launch activities on click 111 | // Intents were historically defined into preferences.xml but as build variants cannot 112 | // inject applicationId as intent packageName properly, it was done this way now... 113 | setOnClickListener(PBConstants.PREF_ABOUT, PBAboutActivity.class); 114 | setOnClickListener(PBConstants.PREF_UPLOAD_JOURNAL, PBJournalActivity.class); 115 | 116 | final PreferenceScreen mediasPreferenceScreen = (PreferenceScreen) findPreference(PBConstants.PREF_MEDIAS_TO_BACKUP); 117 | mediasPreferenceScreen.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { 118 | @Override 119 | public boolean onPreferenceClick(Preference preference) { 120 | checkPermissions(PBConstants.PERM_ORIGIN_MEDIAS); 121 | return true; 122 | } 123 | }); 124 | setHasOptionsMenu(true); 125 | } 126 | 127 | 128 | private void setOnClickListener(final String preferenceKey, final Class cls) { 129 | final Preference uploadJournalPref = findPreference(preferenceKey); 130 | uploadJournalPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { 131 | public boolean onPreferenceClick(Preference preference) { 132 | final Intent intent = new Intent(PBPreferenceFragment.this.getActivity(), cls); 133 | startActivity(intent); 134 | return true; 135 | } 136 | }); 137 | } 138 | 139 | 140 | @Override 141 | public void onResume() { 142 | super.onResume(); 143 | 144 | initPreferences(); 145 | preferences.registerOnSharedPreferenceChangeListener(this); 146 | PBApplication.getMediaStore().addInterface(this); 147 | 148 | if (isPhotoBackupServiceRunning()) { 149 | Intent intent = new Intent(this.getActivity(), PBService.class); 150 | getActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); 151 | isBoundToService = true; 152 | } 153 | 154 | updatePreferences(); 155 | try { 156 | fillBuckets(); 157 | } catch (SecurityException e) { 158 | Log.d(LOG_TAG, e); 159 | } 160 | 161 | } 162 | 163 | 164 | @Override 165 | public void onPause() { 166 | super.onPause(); 167 | preferences.unregisterOnSharedPreferenceChangeListener(this); 168 | PBApplication.getMediaStore().removeInterface(this); 169 | 170 | if (isPhotoBackupServiceRunning() && isBoundToService) { 171 | getActivity().unbindService(serviceConnection); 172 | isBoundToService = false; 173 | } 174 | } 175 | 176 | 177 | @Override 178 | public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) { 179 | 180 | Log.i(LOG_TAG, "onSharedPreferenceChanged: " + key); 181 | if (key.equals(PBConstants.PREF_SERVICE_RUNNING)) { 182 | startOrStopService(sharedPreferences); 183 | 184 | } else if (key.equals(PBConstants.PREF_PICTURE_FOLDER_LIST) || key.equals(PBConstants.PREF_MEDIA_BACKUP_VIDEO)) { 185 | PBApplication.getMediaStore().sync(); 186 | } 187 | setSummaries(); 188 | } 189 | 190 | 191 | @Override 192 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 193 | inflater.inflate(R.menu.main_menu, menu); 194 | menu.findItem(R.id.menu_upload_all).setVisible(isPhotoBackupServiceRunning()); 195 | super.onCreateOptionsMenu(menu, inflater); 196 | } 197 | 198 | 199 | @Override 200 | public boolean onOptionsItemSelected(MenuItem item) { 201 | if (item.getItemId() == R.id.menu_upload_all) { 202 | currentService.sendNextMedia(); 203 | Toast.makeText(getActivity(), R.string.toast_upload_all, Toast.LENGTH_SHORT).show(); 204 | return true; 205 | } 206 | return super.onOptionsItemSelected(item); 207 | } 208 | 209 | 210 | ///////////////////// 211 | // private methods // 212 | ///////////////////// 213 | private void migratePreferences() { 214 | Map allPrefs = preferences.getAll(); 215 | if (allPrefs.containsKey(PBConstants.PREF_WIFI_ONLY)) { 216 | Object obj = allPrefs.get(PBConstants.PREF_WIFI_ONLY); 217 | if (obj instanceof Boolean) { 218 | Log.i(LOG_TAG, "Migrating PREF_WIFI_ONLY for v0.7.0"); 219 | Boolean bool = preferences.getBoolean(PBConstants.PREF_WIFI_ONLY, false); 220 | String wifiOnlyString = bool ? getString(R.string.only_wifi) : getString(R.string.not_only_wifi); 221 | SharedPreferences.Editor editor = preferences.edit(); 222 | editor.putString(PBConstants.PREF_WIFI_ONLY, wifiOnlyString).apply(); 223 | Log.i(LOG_TAG, "Migration done!"); 224 | } 225 | } 226 | 227 | } 228 | 229 | 230 | private void initPreferences() { 231 | ((PBActivity) getActivity()).setActionBar(); 232 | 233 | // switch on if service is running 234 | final SwitchPreference switchPreference = (SwitchPreference) findPreference(PBConstants.PREF_SERVICE_RUNNING); 235 | switchPreference.setChecked(isPhotoBackupServiceRunning()); 236 | 237 | // initiate preferences summaries 238 | setSummaries(); 239 | } 240 | 241 | 242 | private void setSummaries() { 243 | final String wifiOnly = preferences.getString(PBConstants.PREF_WIFI_ONLY, 244 | getResources().getString(R.string.only_wifi_default)); // default 245 | final ListPreference wifiPreference = (ListPreference) findPreference(PBConstants.PREF_WIFI_ONLY); 246 | wifiPreference.setSummary(wifiOnly); 247 | 248 | final String recentUploadOnly = preferences.getString(PBConstants.PREF_RECENT_UPLOAD_ONLY, 249 | getResources().getString(R.string.only_recent_upload_default)); // default 250 | final ListPreference recentUploadPreference = (ListPreference) findPreference(PBConstants.PREF_RECENT_UPLOAD_ONLY); 251 | recentUploadPreference.setSummary(recentUploadOnly); 252 | 253 | final String serverUrl = preferences.getString(PBServerPreferenceFragment.PREF_SERVER_URL, null); 254 | if (serverUrl != null) { 255 | final String serverName = preferences.getString(PBConstants.PREF_SERVER, null); 256 | if (serverName != null) { 257 | final PBServerListPreference serverPreference = (PBServerListPreference) findPreference(PBConstants.PREF_SERVER); 258 | serverPreference.setSummary(serverName + " @ " + serverUrl); 259 | 260 | // bonus: left icon of the server 261 | final int serverNamesId = getResources().getIdentifier("pref_server_names", "array", getActivity().getPackageName()); 262 | final String[] serverNames = getResources().getStringArray(serverNamesId); 263 | final int serverPosition = Arrays.asList(serverNames).indexOf(serverName); 264 | final int serverIconsId = getResources().getIdentifier("pref_server_icons", "array", getActivity().getPackageName()); 265 | final String[] serverIcons = getResources().getStringArray(serverIconsId); 266 | final String serverIcon = serverIcons[serverPosition]; 267 | final String[] parts = serverIcon.split("\\."); 268 | final String drawableName = parts[parts.length - 1]; 269 | final int id = getResources().getIdentifier(drawableName, "drawable", getActivity().getPackageName()); 270 | if (id != 0) { 271 | serverPreference.setIcon(id); 272 | } 273 | } 274 | } 275 | 276 | String bucketSummary = ""; 277 | final Set selectedBuckets = preferences.getStringSet(PBConstants.PREF_PICTURE_FOLDER_LIST, null); 278 | if (selectedBuckets != null && bucketNames != null) { 279 | final ArrayList selectedBucketNames = new ArrayList<>(); 280 | for (String entry : selectedBuckets) { 281 | String oneName = bucketNames.get(entry); 282 | if (oneName != null) { 283 | oneName = oneName.substring(0, oneName.lastIndexOf('(') - 1); 284 | selectedBucketNames.add(oneName); 285 | } 286 | } 287 | bucketSummary = TextUtils.join(", ", selectedBucketNames); 288 | } 289 | final MultiSelectListPreference bucketListPreference = (MultiSelectListPreference) findPreference(PBConstants.PREF_PICTURE_FOLDER_LIST); 290 | bucketListPreference.setSummary(bucketSummary); 291 | } 292 | 293 | 294 | private void startOrStopService(final SharedPreferences preferences) { 295 | boolean userDidStart = preferences.getBoolean(PBConstants.PREF_SERVICE_RUNNING, false); 296 | Log.i(LOG_TAG, "PREF_SERVICE_RUNNING = " + userDidStart); 297 | 298 | if (userDidStart) { 299 | if (validatePreferences()) { 300 | checkPermissions(PBConstants.PERM_ORIGIN_SERVICE); 301 | } else { 302 | final SwitchPreference switchPreference = (SwitchPreference) findPreference(PBConstants.PREF_SERVICE_RUNNING); 303 | switchPreference.setChecked(false); 304 | } 305 | } else if (isPhotoBackupServiceRunning() && currentService != null) { 306 | Log.i(LOG_TAG, "stop PhotoBackup service"); 307 | getActivity().unbindService(serviceConnection); 308 | getActivity().invalidateOptionsMenu(); 309 | isBoundToService = false; 310 | currentService.stopSelf(); 311 | currentService = null; 312 | updatePreferences(); 313 | } 314 | } 315 | 316 | 317 | private void checkPermissions(final int permOrigin) { 318 | permissionOrigin = permOrigin; 319 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 320 | int permissionCheck = ContextCompat.checkSelfPermission(getContext(), 321 | Manifest.permission.READ_EXTERNAL_STORAGE); 322 | if (permissionCheck != PackageManager.PERMISSION_GRANTED) { 323 | ActivityCompat.requestPermissions(getActivity(), 324 | new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 325 | PERMISSION_READ_EXTERNAL_STORAGE); 326 | } else { 327 | Log.i(LOG_TAG, "Permission READ_EXTERNAL_STORAGE already given, cool!"); 328 | didGrantPermission(); // next step 329 | } 330 | } else { // for older Android versions, continue without asking permission 331 | didGrantPermission(); 332 | } 333 | } 334 | 335 | 336 | public void didGrantPermission() { 337 | if (permissionOrigin == PBConstants.PERM_ORIGIN_SERVICE) { 338 | testMediaSender(); 339 | } else if (permissionOrigin == PBConstants.PERM_ORIGIN_MEDIAS) { 340 | fillBuckets(); 341 | } 342 | } 343 | 344 | 345 | private boolean validatePreferences() { 346 | String serverUrl = preferences.getString(PBServerPreferenceFragment.PREF_SERVER_URL, ""); 347 | if (!URLUtil.isValidUrl(serverUrl) || serverUrl.isEmpty()) { 348 | Toast.makeText(getActivity(), R.string.toast_urisyntaxexception, Toast.LENGTH_LONG).show(); 349 | return false; 350 | } 351 | 352 | String serverPassHash = preferences.getString(PBServerPreferenceFragment.PREF_SERVER_PASS_HASH, ""); 353 | if (serverPassHash.isEmpty()) { 354 | Toast.makeText(getActivity(), R.string.toast_serverpassempty, Toast.LENGTH_LONG).show(); 355 | return false; 356 | } 357 | 358 | return true; 359 | } 360 | 361 | 362 | // Returns the current state of the PhotoBackup Service 363 | // See http://stackoverflow.com/a/5921190/417006 364 | private boolean isPhotoBackupServiceRunning() { 365 | final Activity activity = getActivity(); 366 | if (activity != null) { 367 | final ActivityManager manager = (ActivityManager) getActivity().getSystemService(Context.ACTIVITY_SERVICE); 368 | for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { 369 | boolean sameClass = PBService.class.getName().equals(service.service.getClassName()); 370 | boolean samePackage = BuildConfig.APPLICATION_ID.equals(service.service.getPackageName()); 371 | if (sameClass && samePackage) { 372 | Log.i(LOG_TAG, "Service is running"); 373 | return true; 374 | } 375 | } 376 | } 377 | Log.i(LOG_TAG, "Service or activity is NOT running"); 378 | return false; 379 | } 380 | 381 | 382 | private void updatePreferences() { 383 | try { 384 | final Preference uploadJournalPref = findPreference(PBConstants.PREF_UPLOAD_JOURNAL); 385 | // service is running 386 | if (isPhotoBackupServiceRunning() && currentService != null) { 387 | int nbMedia = PBApplication.getMediaStore().getMediaList().size(); 388 | uploadJournalPref.setTitle(this.getResources().getString(R.string.journal_title) + " (" + nbMedia + ")"); 389 | uploadJournalPref.setEnabled(nbMedia > 0); 390 | // service is not running 391 | } else { 392 | uploadJournalPref.setTitle(getResources().getString(R.string.journal_noaccess)); 393 | uploadJournalPref.setEnabled(false); 394 | } 395 | } catch (IllegalStateException e) { 396 | Log.w(LOG_TAG, e); 397 | } 398 | } 399 | 400 | 401 | private void fillBuckets() { 402 | this.bucketNames = PBApplication.getMediaStore().getBucketData(); 403 | 404 | final CharSequence[] ids = this.bucketNames.values().toArray(new CharSequence[this.bucketNames.size()]); 405 | final CharSequence[] names = this.bucketNames.keySet().toArray(new CharSequence[this.bucketNames.size()]); 406 | 407 | final MultiSelectListPreference bucketListPreference = (MultiSelectListPreference) findPreference(PBConstants.PREF_PICTURE_FOLDER_LIST); 408 | bucketListPreference.setEntries(ids); 409 | bucketListPreference.setEnabled(true); 410 | bucketListPreference.setEntryValues(names); 411 | 412 | setSummaries(); 413 | } 414 | 415 | 416 | //////////////////// 417 | // public methods // 418 | //////////////////// 419 | private void testMediaSender() { 420 | Log.i(LOG_TAG, "start PhotoBackup service"); 421 | PBMediaSender mediaSender = new PBMediaSender(); 422 | mediaSender.addInterface(this); 423 | mediaSender.test(); 424 | } 425 | 426 | 427 | //////////////////////////////////// 428 | // PBMediaStoreListener callbacks // 429 | //////////////////////////////////// 430 | public void onSyncMediaStoreTaskPostExecute() { 431 | updatePreferences(); 432 | } 433 | 434 | 435 | /////////////////////////////////// 436 | // PBMediaSenderEvents callbacks // 437 | /////////////////////////////////// 438 | public void onMessage(final String message) { 439 | getActivity().runOnUiThread(new Runnable() { 440 | @Override 441 | public void run() { 442 | Toast.makeText(PBApplication.getApp(), message, Toast.LENGTH_SHORT).show(); 443 | } 444 | }); 445 | } 446 | 447 | 448 | public void onTestSuccess() { 449 | final Intent serviceIntent = new Intent(getActivity(), PBService.class); 450 | getActivity().startService(serviceIntent); 451 | getActivity().bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); 452 | isBoundToService = true; 453 | getActivity().invalidateOptionsMenu(); 454 | } 455 | 456 | 457 | public void onTestFailure() { 458 | getActivity().runOnUiThread(new Runnable() { 459 | @Override 460 | public void run() { 461 | final SwitchPreference switchPreference = (SwitchPreference) findPreference(PBConstants.PREF_SERVICE_RUNNING); 462 | switchPreference.setChecked(false); 463 | } 464 | }); 465 | } 466 | 467 | 468 | public void onSendSuccess() { 469 | // Do nothing 470 | } 471 | public void onSendFailure() { 472 | // Do nothing 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/preferences/PBServerListPreference.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | *

4 | * This file is part of PhotoBackup. 5 | *

6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | *

11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | *

16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup.preferences; 20 | 21 | import android.app.Activity; 22 | import android.app.AlertDialog; 23 | import android.app.Dialog; 24 | import android.app.FragmentTransaction; 25 | import android.content.Context; 26 | import android.content.DialogInterface; 27 | import android.content.SharedPreferences; 28 | import android.os.Bundle; 29 | import android.preference.ListPreference; 30 | import android.preference.PreferenceManager; 31 | import android.util.AttributeSet; 32 | import android.view.LayoutInflater; 33 | import android.view.View; 34 | import android.view.ViewGroup; 35 | import android.widget.BaseAdapter; 36 | import android.widget.ImageView; 37 | import android.widget.TextView; 38 | 39 | import fr.s13d.photobackup.Log; 40 | import fr.s13d.photobackup.PBApplication; 41 | import fr.s13d.photobackup.PBConstants; 42 | import fr.s13d.photobackup.R; 43 | 44 | public class PBServerListPreference extends ListPreference { 45 | 46 | private static final String LOG_TAG = "PBServerListPreference"; 47 | private final Context context; 48 | private CharSequence[] servers; 49 | 50 | 51 | ////////////////// 52 | // Constructors // 53 | ////////////////// 54 | public PBServerListPreference(Context theContext, AttributeSet attrs) { 55 | super(theContext, attrs); 56 | context = theContext; 57 | } 58 | 59 | 60 | ////////////// 61 | // Override // 62 | ////////////// 63 | @Override 64 | protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { 65 | servers = getEntryValues(); 66 | 67 | if (servers == null) { 68 | throw new IllegalStateException("ListPreference requires an entryValues array!"); 69 | } 70 | 71 | ListPreferenceAdapter listPreferenceAdapter = new ListPreferenceAdapter(); 72 | builder.setPositiveButton(null, null); 73 | builder.setAdapter(listPreferenceAdapter, new DialogInterface.OnClickListener() { 74 | public void onClick(DialogInterface dialog, int which) { 75 | Log.i(LOG_TAG, "clicked"); 76 | } 77 | }); 78 | } 79 | 80 | 81 | ///////////// 82 | // private // 83 | ///////////// 84 | private class ListPreferenceAdapter extends BaseAdapter { 85 | ListPreferenceAdapter() { 86 | // Do nothing 87 | } 88 | 89 | public int getCount() { 90 | return servers.length; 91 | } 92 | 93 | public Object getItem(int position) { 94 | return position; 95 | } 96 | 97 | public long getItemId(int position) { 98 | return position; 99 | } 100 | 101 | public View getView(final int position, View view, ViewGroup parent) { 102 | View row = view; 103 | if (row == null) { 104 | row = LayoutInflater.from(context).inflate(R.layout.server_list_row, parent, false); 105 | RowHolder holder = new RowHolder(position, row); 106 | 107 | row.setTag(holder); 108 | row.setOnClickListener(new View.OnClickListener() { 109 | public void onClick(View v) { 110 | Log.i(LOG_TAG, "clicked on: " + servers[position]); 111 | clickedAtPosition(position); 112 | 113 | Dialog mDialog = getDialog(); 114 | mDialog.dismiss(); 115 | } 116 | }); 117 | } 118 | 119 | return row; 120 | } 121 | 122 | 123 | private void clickedAtPosition(int position) { 124 | // store the preference as the position 125 | final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(PBApplication.getApp()); 126 | preferences.edit().putString(PBConstants.PREF_SERVER, servers[position].toString()).apply(); 127 | 128 | // build new preference fragment from server position in list 129 | Bundle fragmentArguments = new Bundle(1); 130 | fragmentArguments.putString(PBServerPreferenceFragment.PREF_SERVER_NAME, 131 | servers[position].toString()); 132 | PBServerPreferenceFragment fragment = new PBServerPreferenceFragment(); 133 | fragment.setArguments(fragmentArguments); 134 | 135 | // put it on back stack 136 | final Activity activity = (Activity) context; 137 | FragmentTransaction transaction = activity.getFragmentManager().beginTransaction(); 138 | transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 139 | transaction.replace(android.R.id.content, fragment); 140 | transaction.addToBackStack(null); 141 | transaction.commit(); 142 | } 143 | 144 | 145 | class RowHolder { 146 | private TextView text = null; 147 | 148 | RowHolder(int position, View row) { 149 | text = (TextView) row.findViewById(R.id.servername); 150 | text.setText(servers[position]); 151 | setRowIcon(position, row); 152 | } 153 | 154 | private void setRowIcon(final int position, final View row) { 155 | final int arrayId = context.getResources().getIdentifier("pref_server_icons", "array", context.getPackageName()); 156 | final String[] stringArray = context.getResources().getStringArray(arrayId); 157 | final String drawablePath = stringArray[position]; 158 | final String[] parts = drawablePath.split("\\."); 159 | final String drawableName = parts[parts.length - 1]; 160 | final int drawableId = context.getResources().getIdentifier(drawableName, 161 | "drawable", context.getPackageName()); 162 | if (drawableId != 0) { 163 | final ImageView imageView = (ImageView) row.findViewById(R.id.thumbnail); 164 | imageView.setImageResource(drawableId); 165 | } 166 | } 167 | 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /app/src/main/java/fr/s13d/photobackup/preferences/PBServerPreferenceFragment.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2016 Stéphane Péchard. 3 | * 4 | * This file is part of PhotoBackup. 5 | * 6 | * PhotoBackup is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * PhotoBackup is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | package fr.s13d.photobackup.preferences; 20 | 21 | import android.content.SharedPreferences; 22 | import android.os.Bundle; 23 | import android.preference.EditTextPreference; 24 | import android.preference.Preference; 25 | import android.preference.PreferenceFragment; 26 | import android.preference.PreferenceManager; 27 | import android.preference.PreferenceScreen; 28 | import android.view.LayoutInflater; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | 32 | import java.security.MessageDigest; 33 | import java.security.NoSuchAlgorithmException; 34 | 35 | import fr.s13d.photobackup.Log; 36 | import fr.s13d.photobackup.R; 37 | 38 | 39 | public class PBServerPreferenceFragment extends PreferenceFragment 40 | implements SharedPreferences.OnSharedPreferenceChangeListener { 41 | 42 | private static final String LOG_TAG = "PBServerPreferenceFragment"; 43 | private String serverName; 44 | private SharedPreferences preferences; 45 | 46 | public static final String PREF_SERVER_NAME = "PREF_SERVER_NAME"; 47 | public static final String PREF_SERVER_URL = "PREF_SERVER_URL"; 48 | private static final String PREF_SERVER_PASS = "PREF_SERVER_PASS"; 49 | public static final String PREF_SERVER_PASS_HASH = "PREF_SERVER_PASS_HASH"; 50 | public static final String PREF_SERVER_HTTP_AUTH_SWITCH = "PREF_SERVER_HTTP_AUTH_SWITCH"; 51 | public static final String PREF_SERVER_HTTP_AUTH_LOGIN = "PREF_SERVER_HTTP_AUTH_LOGIN"; 52 | public static final String PREF_SERVER_HTTP_AUTH_PASS = "PREF_SERVER_HTTP_AUTH_PASS"; 53 | 54 | 55 | ////////////////// 56 | // Constructors // 57 | ////////////////// 58 | public PBServerPreferenceFragment() { 59 | // Do nothing 60 | } 61 | 62 | 63 | ////////////// 64 | // Override // 65 | ////////////// 66 | @Override 67 | public void onCreate(final Bundle savedInstanceState) { 68 | super.onCreate(savedInstanceState); 69 | addPreferencesFromResource(R.xml.server_preferences); 70 | 71 | preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); 72 | } 73 | 74 | 75 | @Override 76 | public void onResume() { 77 | super.onResume(); 78 | preferences.registerOnSharedPreferenceChangeListener(this); 79 | setSummaries(); 80 | } 81 | 82 | 83 | @Override 84 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 85 | final View view = super.onCreateView(inflater, container, savedInstanceState); 86 | 87 | final Bundle bundle = getArguments(); 88 | if (bundle!= null && bundle.containsKey(PREF_SERVER_NAME)) { 89 | serverName = bundle.getString(PREF_SERVER_NAME); 90 | configurePreference(); 91 | } 92 | return view; 93 | } 94 | 95 | 96 | @Override 97 | public void onPause() { 98 | super.onPause(); 99 | if (preferences != null) { 100 | preferences.unregisterOnSharedPreferenceChangeListener(this); 101 | } 102 | } 103 | 104 | 105 | @Override 106 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 107 | 108 | Log.i(LOG_TAG, "onSharedPreferenceChanged: " + key); 109 | 110 | setSummaries(); 111 | if (key.equals(PREF_SERVER_PASS)) { 112 | final String pass = sharedPreferences.getString(PREF_SERVER_PASS, ""); 113 | if (!pass.isEmpty()) { 114 | createAndSetServerPass(sharedPreferences); 115 | } 116 | 117 | } else if (sharedPreferences == null) { 118 | Log.e(LOG_TAG, "Error: preferences == null"); 119 | } 120 | 121 | } 122 | 123 | 124 | ///////////////////// 125 | // Private methods // 126 | ///////////////////// 127 | private void configurePreference() { 128 | // title and back button of the action bar 129 | getActivity().setTitle(serverName + " server settings"); 130 | if (getActivity().getActionBar() != null) { 131 | getActivity().getActionBar().setDisplayHomeAsUpEnabled(true); 132 | } 133 | 134 | // save server name into the preferences 135 | final SharedPreferences.Editor preferencesEditor = preferences.edit(); 136 | preferencesEditor.putString(PREF_SERVER_NAME, serverName).apply(); 137 | 138 | // access configuration in servers_params.xml file 139 | final int arrayId = getActivity().getResources().getIdentifier(serverName, "array", getActivity().getPackageName()); 140 | final String[] serverPrefsToRemove = getActivity().getResources().getStringArray(arrayId); 141 | 142 | // remove unused preferences given in xml list 143 | final PreferenceScreen screen = (PreferenceScreen) findPreference(LOG_TAG); 144 | for (String param : serverPrefsToRemove) { 145 | Log.i(LOG_TAG, "Remove preference named: " + param); 146 | final Preference pref = findPreference(param); 147 | screen.removePreference(pref); 148 | } 149 | 150 | } 151 | 152 | 153 | private void setSummaries() { 154 | final EditTextPreference urlPreference = (EditTextPreference) findPreference(PREF_SERVER_URL); 155 | urlPreference.setSummary(preferences.getString(PREF_SERVER_URL, this.getResources().getString(R.string.server_url_summary))); 156 | 157 | final String serverPassHash = preferences.getString(PREF_SERVER_PASS_HASH, ""); 158 | final EditTextPreference serverPassTextPreference = (EditTextPreference) findPreference(PREF_SERVER_PASS); 159 | if (serverPassHash.isEmpty()) { 160 | serverPassTextPreference.setSummary(getResources().getString(R.string.server_password_summary)); 161 | } else { 162 | serverPassTextPreference.setSummary(getResources().getString(R.string.server_password_summary_set)); 163 | } 164 | 165 | final EditTextPreference httpLoginPreference = (EditTextPreference) findPreference(PREF_SERVER_HTTP_AUTH_LOGIN); 166 | httpLoginPreference.setSummary(preferences.getString(PREF_SERVER_HTTP_AUTH_LOGIN, "")); 167 | 168 | final String httpPass = preferences.getString(PREF_SERVER_HTTP_AUTH_PASS,""); 169 | if (!httpPass.isEmpty()) { 170 | final EditTextPreference httpPassPreference = (EditTextPreference) findPreference(PREF_SERVER_HTTP_AUTH_PASS); 171 | httpPassPreference.setSummary(getResources().getString(R.string.server_password_summary_set)); 172 | } 173 | } 174 | 175 | 176 | private void createAndSetServerPass(final SharedPreferences sharedPreferences) { 177 | // store only the hash of the password in the preferences 178 | MessageDigest md; 179 | try { 180 | md = MessageDigest.getInstance("SHA-512"); 181 | } catch (NoSuchAlgorithmException e) { 182 | Log.e(LOG_TAG, e); 183 | return; 184 | } 185 | 186 | final String pass = sharedPreferences.getString(PREF_SERVER_PASS, null); 187 | if (pass == null) { 188 | return; 189 | } 190 | 191 | // compute the hash 192 | md.update(pass.getBytes()); 193 | final byte[] mb = md.digest(); 194 | final StringBuilder sbHash = new StringBuilder(); 195 | for (final byte temp : mb) { 196 | final StringBuilder sb = new StringBuilder(Integer.toHexString(temp)); 197 | if (sb.length() == 0) { 198 | sb.insert(0, "00"); 199 | } else if (sb.length() == 1) { 200 | sb.insert(0, '0'); 201 | } 202 | sbHash.append(sb.substring(sb.length() - 2)); 203 | } 204 | 205 | final SharedPreferences.Editor preferencesEditor = preferences.edit(); 206 | // set the hash in the preferences 207 | preferencesEditor.putString(PREF_SERVER_PASS_HASH, sbHash.toString()).apply(); 208 | // remove the real password from the preferences, for security 209 | preferencesEditor.putString(PREF_SERVER_PASS, "").apply(); 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_backup_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-hdpi/ic_backup_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_done_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-hdpi/ic_done_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_error_outline_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-hdpi/ic_error_outline_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_file_upload_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-hdpi/ic_file_upload_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_backup_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-mdpi/ic_backup_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_done_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-mdpi/ic_done_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_error_outline_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-mdpi/ic_error_outline_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_file_upload_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-mdpi/ic_file_upload_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_backup_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-xhdpi/ic_backup_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_done_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-xhdpi/ic_done_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_error_outline_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-xhdpi/ic_error_outline_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_file_upload_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-xhdpi/ic_file_upload_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_backup_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-xxhdpi/ic_backup_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_done_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-xxhdpi/ic_done_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_error_outline_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-xxhdpi/ic_error_outline_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_file_upload_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-xxhdpi/ic_file_upload_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_backup_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-xxxhdpi/ic_backup_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_done_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-xxxhdpi/ic_done_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_error_outline_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-xxxhdpi/ic_error_outline_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_file_upload_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable-xxxhdpi/ic_file_upload_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhotoBackup/client-android/2d632b8012082585a7d9570a4265fa441bfcd878/app/src/main/res/drawable/icon.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 20 | 21 | 27 | 28 | 36 | 37 | 44 | 45 | 48 | 49 |