diff --git a/.travis.yml b/.travis.yml index 7c7f76eaa..20c5bfaee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,14 +17,17 @@ jdk: android: components: - - platform-tools - tools - - build-tools-26.0.1 + - platform-tools + - build-tools-26.0.2 - extra-google-m2repository - extra-android-m2repository - ${ANDROID_TARGET} - android-25 + - android-26 - sys-img-${ANDROID_ABI}-${ANDROID_TARGET} + licenses: + - 'android-sdk-license-.+' before_script: - echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI @@ -32,14 +35,16 @@ before_script: - android-wait-for-emulator script: - - ./gradlew clean check connectedCheck jacocoTestReport --stacktrace + - ./gradlew clean check connectedCheck jacocoTestReport after_success: - bash <(curl -s https://codecov.io/bash) after_failure: - - echo '*** Connected Test Rsults ***' - - w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/*Test.html + - echo '*** Debug Unit Test Results ***' + - w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/tests/*/classes/*Test.html + - echo '*** Connected Test Results ***' + - w3m -dump ${TRAVIS_BUILD_DIR}/app/build/reports/androidTests/connected/flavors/*/*Test.html before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de06a4e3..035835839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,48 @@ # Wikimedia Commons for Android -## v2.5.0 beta +## v2.6.7 +- Added null checks to prevent frequent crashes in ModificationsSyncAdapter + +## v2.6.6 +- Refactored Dagger to fix crashes encountered in production +- Fixed "?" displaying in description of Nearby places +- Database-related cleanup and tests +- Optimized dimens.xml +- Fixed issue where map opens with incorrect coordinates + +## v2.6.5 beta +- Changed "send log" feature to only send logs to private Google group forum +- Switched to using Wikimedia maps server instead of Mapbox for privacy reasons +- Removed event logging from app for privacy reasons +- Fixed crash caused by rapidly switching from Nearby map to list while loading + +## v2.6.4 beta +- Excluded httpclient and commons-logging to fix release build errors +- Fixed crashes caused by Fresco and Dagger + +## v2.6.3 beta +- Same as 2.6.2 except with localizations added for Google Code-In + +## v2.6.2 beta +- Reverted temporarily to last stable version while working on crash fix + +## v2.6.1 beta +- Failed attempt to fix crashes in release build with the previous beta release + +## v2.6.0 beta +- Multiple bugfixes for location updates and list/map loading in Nearby +- Multiple fixes for various crashes and memory leaks +- Added several unit tests +- Modified About page to include WMF disclaimer and modified Privacy Policy link to point to our individual privacy policy +- Added option for users to send logs to developers (has to be manually activated by user) +- Converted PNGs to WebPs +- Improved login screen with new design and privacy policy link +- Improved category display, if a category has an exact name entered, it will be shown first +- New UI for Nearby list +- Added product flavors for production and the beta-cluster Wikimedia servers +- Various improvements to navigation flow and backstack + +## v2.5.0 beta - Added one-time popup for beta users to provide feedback on IEG renewal proposal - Added link to Commons policies in ShareActivity - Various string fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..ee7f42e06 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +Please see our guidelines in the wiki: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21 diff --git a/CREDITS b/CREDITS index b404302fb..a4a4ae0f5 100644 --- a/CREDITS +++ b/CREDITS @@ -29,6 +29,7 @@ their contribution to the product. * Jan Piotrowski * Bruke Mekuria Mulugeta * Paul Hawke +* Vishan Seru 3rd party open source libraries used: * Butterknife @@ -38,3 +39,953 @@ their contribution to the product. 3rd party open source apps from which significant code has been reused: * Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia + +=========================================================================== + +The Wikimedia Commons Android app uses portions of MapBox. + +mapbox-gl-native copyright (c) 2014-2018 Mapbox. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================== + +Mapbox GL uses portions of Android Gesture Detectors Framework. + +Copyright (c) 2012, Almer Thie + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================== + +Mapbox GL uses portions of Android Support Library. + +Copyright (c) 2005-2013, The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +=========================================================================== + +Mapbox GL uses portions of Boost. + +Distributed under the Boost Software License, Version 1.0. + +http://www.boost.org/LICENSE_1_0.txt + +=========================================================================== + +Mapbox GL uses portions of Clipper. + +Author : Angus Johnson +Version : 6.1.3a +Date : 22 January 2014 +Website : http://www.angusj.com +Copyright : Angus Johnson 2010-2014 + +License: +Use, modification & distribution is subject to Boost Software License Ver 1. +http://www.boost.org/LICENSE_1_0.txt + +Attributions: +The code in this library is an extension of Bala Vatti's clipping algorithm: +"A generic solution to polygon clipping" +Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. +http://portal.acm.org/citation.cfm?id=129906 + +Computer graphics and geometric modeling: implementation and algorithms +By Max K. Agoston +Springer; 1 edition (January 4, 2005) +http://books.google.com/books?q=vatti+clipping+agoston + +See also: +"Polygon Offsetting by Computing Winding Numbers" +Paper no. DETC2005-85513 pp. 565-575 +ASME 2005 International Design Engineering Technical Conferences +and Computers and Information in Engineering Conference (IDETC/CIE2005) +September 24-28, 2005 , Long Beach, California, USA +http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf + +=========================================================================== + +Mapbox GL uses portions of BugshotKit. + +The MIT License (MIT) + +Copyright (c) 2014 marcoarment + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=========================================================================== + +Mapbox GL uses portions of CSS Color Parser. + +(c) Dean McNamee , 2012. +C++ port by Konstantin Käfer , 2014. + +https://github.com/deanm/css-color-parser-js +https://github.com/kkaefer/css-color-parser-cpp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +=========================================================================== + +Mapbox GL uses portions of GLFW. + +Copyright (c) 2002-2006 Marcus Geelnard +Copyright (c) 2006-2010 Camilla Berglund + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +=========================================================================== + +Mapbox GL uses portions of libc++. + +The libc++ library is dual licensed under both the University of Illinois +"BSD-Like" license and the MIT license. As a user of this code you may choose +to use it under either license. As a contributor, you agree to allow your code +to be used under both. + +Full text of the relevant licenses is included below. + +==== + +University of Illinois/NCSA +Open Source License + +Copyright (c) 2009-2015 by the contributors listed in CREDITS.TXT + +All rights reserved. + +Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + +* Neither the names of the LLVM Team, University of Illinois at + Urbana-Champaign, nor the names of its contributors may be used to + endorse or promote products derived from this Software without specific + prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. + +==== + +Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +=========================================================================== + +Mapbox GL uses portions of libcurl. + +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1996 - 2015, Daniel Stenberg, . + +All rights reserved. + +Permission to use, copy, modify, and distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright +notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall not +be used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization of the copyright holder. + +=========================================================================== + +Mapbox GL uses portions of libjpeg-turbo. + +This software is based in part on the work of the Independent JPEG Group. + +Copyright (C)2009-2015 D. R. Commander. All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +- Neither the name of the libjpeg-turbo Project nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +TurboJPEG/LJT: this implements the TurboJPEG API using libjpeg or libjpeg-turbo + +=========================================================================== + +Mapbox GL uses portions of libpng. + +This copy of the libpng notices is provided for your convenience. In case of +any discrepancy between this copy and the notices in the file png.h that is +included in the libpng distribution, the latter shall prevail. + +COPYRIGHT NOTICE, DISCLAIMER, and LICENSE: + +If you modify libpng you may insert additional notices immediately following +this sentence. + +This code is released under the libpng license. + +libpng versions 1.0.7, July 1, 2000, through 1.6.18, July 23, 2015, are +Copyright (c) 2000-2002, 2004, 2006-2015 Glenn Randers-Pehrson, and are +distributed according to the same disclaimer and license as libpng-1.0.6 +with the following individuals added to the list of Contributing Authors: + + Simon-Pierre Cadieux + Eric S. Raymond + Mans Rullgard + Cosmin Truta + Gilles Vollant + James Yu + +and with the following additions to the disclaimer: + + There is no warranty against interference with your enjoyment of the + library or against infringement. There is no warranty that our + efforts or the library will fulfill any of your particular purposes + or needs. This library is provided with all faults, and the entire + risk of satisfactory quality, performance, accuracy, and effort is with + the user. + +libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are +Copyright (c) 1998-2000 Glenn Randers-Pehrson, and are distributed according +to the same disclaimer and license as libpng-0.96, with the following +individuals added to the list of Contributing Authors: + + Tom Lane + Glenn Randers-Pehrson + Willem van Schaik + +libpng versions 0.89, June 1996, through 0.96, May 1997, are +Copyright (c) 1996-1997 Andreas Dilger, and are +distributed according to the same disclaimer and license as libpng-0.88, +with the following individuals added to the list of Contributing Authors: + + John Bowler + Kevin Bracey + Sam Bushell + Magnus Holmgren + Greg Roelofs + Tom Tanner + +libpng versions 0.5, May 1995, through 0.88, January 1996, are +Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +For the purposes of this copyright and license, "Contributing Authors" +is defined as the following set of individuals: + + Andreas Dilger + Dave Martindale + Guy Eric Schalnat + Paul Schmidt + Tim Wegner + +The PNG Reference Library is supplied "AS IS". The Contributing Authors +and Group 42, Inc. disclaim all warranties, expressed or implied, +including, without limitation, the warranties of merchantability and of +fitness for any purpose. The Contributing Authors and Group 42, Inc. +assume no liability for direct, indirect, incidental, special, exemplary, +or consequential damages, which may result from the use of the PNG +Reference Library, even if advised of the possibility of such damage. + +Permission is hereby granted to use, copy, modify, and distribute this +source code, or portions hereof, for any purpose, without fee, subject +to the following restrictions: + +1. The origin of this source code must not be misrepresented. + +2. Altered versions must be plainly marked as such and must not + be misrepresented as being the original source. + +3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + +The Contributing Authors and Group 42, Inc. specifically permit, without +fee, and encourage the use of this source code as a component to +supporting the PNG file format in commercial products. If you use this +source code in a product, acknowledgment is not required but would be +appreciated. + +=========================================================================== + +Mapbox GL uses portions of libuv. + +libuv is part of the Node project: http://nodejs.org/ +libuv may be distributed alone under Node's license: + +==== + +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +==== + +This license applies to all parts of libuv that are not externally +maintained libraries. + +The externally maintained libraries used by libuv are: + +- tree.h (from FreeBSD), copyright Niels Provos. Two clause BSD license. + +- inet_pton and inet_ntop implementations, contained in src/inet.c, are + copyright the Internet Systems Consortium, Inc., and licensed under the ISC + license. + +- stdint-msvc2008.h (from msinttypes), copyright Alexander Chemeris. Three + clause BSD license. + +- pthread-fixes.h, pthread-fixes.c, copyright Google Inc. and Sony Mobile + Communications AB. Three clause BSD license. + +- android-ifaddrs.h, android-ifaddrs.c, copyright Berkeley Software Design + Inc, Kenneth MacKay and Emergya (Cloud4all, FP7/2007-2013, grant agreement + n° 289016). Three clause BSD license. + +=========================================================================== + +Mapbox GL uses portions of libzip. + +Copyright (C) 1999-2014 Dieter Baron and Thomas Klausner + +The authors can be contacted at + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +3. The names of the authors may not be used to endorse or promote + products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================== + +Mapbox GL uses portions of LOST. + +Copyright (c) 2014 Mapzen + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +=========================================================================== + +Mapbox GL uses portions of the Mapbox iOS SDK, which was derived from the +Route-Me open source project, including the Alpstein fork of it. + +The Route-Me license appears below. + +Copyright (c) 2008-2013, Route-Me Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +=========================================================================== + +Mapbox GL uses portions of nunicode. + +Copyright (c) 2013 Aleksey Tulinov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +=========================================================================== + +Mapbox GL uses portions of OkHTTP. + +Copyright 2014 Square, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +=========================================================================== + +Mapbox GL uses portions of OpenSSL. + +LICENSE ISSUES +============== + +The OpenSSL toolkit stays under a dual license, i.e. both the conditions of +the OpenSSL License and the original SSLeay license apply to the toolkit. +See below for the actual license texts. Actually both licenses are BSD-style +Open Source licenses. In case of any license issues related to OpenSSL +please contact openssl-core@openssl.org. + +OpenSSL License +--------------- + +Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +3. All advertising materials mentioning features or use of this + software must display the following acknowledgment: + "This product includes software developed by the OpenSSL Project + for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + +4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + endorse or promote products derived from this software without + prior written permission. For written permission, please contact + openssl-core@openssl.org. + +5. Products derived from this software may not be called "OpenSSL" + nor may "OpenSSL" appear in their names without prior written + permission of the OpenSSL Project. + +6. Redistributions of any form whatsoever must retain the following + acknowledgment: + "This product includes software developed by the OpenSSL Project + for use in the OpenSSL Toolkit (http://www.openssl.org/)" + +THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY +EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR +ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +OF THE POSSIBILITY OF SUCH DAMAGE. + +This product includes cryptographic software written by Eric Young +(eay@cryptsoft.com). This product includes software written by Tim +Hudson (tjh@cryptsoft.com). + +Original SSLeay License +----------------------- + +Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) +All rights reserved. + +This package is an SSL implementation written +by Eric Young (eay@cryptsoft.com). +The implementation was written so as to conform with Netscapes SSL. + +This library is free for commercial and non-commercial use as long as +The following conditions are aheared to. The following conditions +apply to all code found in this distribution, be it the RC4, RSA, +lhash, DES, etc., code; not just the SSL code. The SSL documentation +included with this distribution is covered by the same copyright terms +except that the holder is Tim Hudson (tjh@cryptsoft.com). + +Copyright remains Eric Young's, and as such any Copyright notices in +the code are not to be removed. +If this package is used in a product, Eric Young should be given attribution +as the author of the parts of the library used. +This can be in the form of a textual message at program startup or +in documentation (online or textual) provided with the package. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + "This product includes cryptographic software written by + Eric Young (eay@cryptsoft.com)" + The word 'cryptographic' can be left out if the rouines from the library + being used are not cryptographic related :-). +4. If you include any Windows specific code (or a derivative thereof) from + the apps directory (application code) you must include an acknowledgement: + "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + +THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +The licence and distribution terms for any publically available version or +derivative of this code cannot be changed. i.e. this code cannot simply be +copied and put under another distribution licence +[including the GNU Public Licence.] + +=========================================================================== + +Mapbox GL uses portions of RapidJSON. + +Tencent is pleased to support the open source community by making RapidJSON +available. + +Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights +reserved. + +If you have downloaded a copy of the RapidJSON binary from Tencent, please note +that the RapidJSON binary is licensed under the MIT License. If you have +downloaded a copy of the RapidJSON source code from Tencent, please note that +RapidJSON source code is licensed under the MIT License, except for the third- +party components listed below which are subject to different license terms. +Your integration of RapidJSON into your own projects may require compliance with +the MIT License, as well as the other licenses applicable to the third-party +components included within RapidJSON. To avoid the problematic JSON license in +your own projects, it's sufficient to exclude the bin/jsonchecker/ directory, as +it's the only code under the JSON license. A copy of the MIT License is included +in this file. + +Other dependencies and licenses: + +Open Source Software Licensed Under the BSD License: +-------------------------------------------------------------------- + +The msinttypes r29 +Copyright (c) 2006-2013 Alexander Chemeris +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of copyright holder nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Open Source Software Licensed Under the JSON License: +-------------------------------------------------------------------- + +json.org +Copyright (c) 2002 JSON.org +All Rights Reserved. + +JSON_checker +Copyright (c) 2002 JSON.org +All Rights Reserved. + +Terms of the JSON License: +--------------------------------------------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Terms of the MIT License: +-------------------------------------------------------------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=========================================================================== + +Mapbox GL uses portions of Reachability. + +Copyright (c) 2011, Tony Million. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +=========================================================================== + +Mapbox GL uses portions of SQLite. + +2001 September 15 + +The author disclaims copyright to this source code. In place of +a legal notice, here is a blessing: + + May you do good and not evil. + May you find forgiveness for yourself and forgive others. + May you share freely, never taking more than you give. + +=========================================================================== + +Mapbox GL uses portions of SVPulsingAnnotationView. + +Copyright (c) 2013, Sam Vermette + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +=========================================================================== + +Mapbox GL uses portions of zlib. + +Acknowledgments: + +The deflate format used by zlib was defined by Phil Katz. The deflate and +zlib specifications were written by L. Peter Deutsch. Thanks to all the +people who reported problems and suggested various improvements in zlib; they +are too numerous to cite here. + +Copyright notice: + +(C) 1995-2013 Jean-loup Gailly and Mark Adler + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +=========================================================================== + +Mapbox GL uses portions of Realm Objective-C. + +Copyright 2015 Realm Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..d8ef17752 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,35 @@ +_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby `) or just by typing keywords into the search filter._ + +**Summary:** + +Summarize your issue in one sentence (what goes wrong, what did you expect to happen) + +**Steps to reproduce:** + +How can we reproduce the issue? + +**Add System logs:** + +Add logcat files here (if possible). + +**Expected behavior:** + +What did you expect the App to do? + +**Observed behavior:** + +What did you see instead? Describe your issue in detail here. + +**Device and Android version:** + +What make and model device (e.g., Samsung J7) did you encounter this on? What Android +version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are you running? Is it + the stock version from the manufacturer or a custom ROM ? + + **Commons app version:** + +You can find this information by going to the navigation drawer in the app and tapping 'About' + +**Screen-shots:** + +Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher. diff --git a/README.md b/README.md index 6ac8dc892..696201029 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2]. -Initially started by the Wikimedia Foundation, this app is now maintained by volunteers. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request :-) - -We are currently applying for an [IEG renewal][10] to work on the app for the next 6 months. Feedback is very much welcomed. +Initially started by the Wikimedia Foundation, this app is now maintained by grantees and volunteers of the Wikimedia community. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request :-) Get it on F-Droid diff --git a/app/build.gradle b/app/build.gradle index 56a4694af..98693126e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,58 +1,78 @@ +apply from: '../gitutils.gradle' apply plugin: 'com.android.application' -apply plugin: 'me.tatarka.retrolambda' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' apply plugin: 'jacoco-android' apply from: 'quality.gradle' apply plugin: 'com.getkeepsafe.dexcount' dependencies { - compile 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07' - compile 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' - compile 'in.yuvi:http.fluent:1.3' - compile 'com.android.volley:volley:1.0.0' - compile 'ch.acra:acra:4.7.0' - compile 'org.mediawiki:api:1.3' - compile 'commons-codec:commons-codec:1.10' - compile 'com.github.pedrovgs:renderers:3.3.3' - compile 'com.google.code.gson:gson:2.8.0' - compile 'com.jakewharton.timber:timber:4.5.1' - compile 'info.debatty:java-string-similarity:0.24' - compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.1.0@aar'){ + implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07' + implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' + implementation 'in.yuvi:http.fluent:1.3' + implementation 'com.android.volley:volley:1.0.0' + implementation 'ch.acra:acra:4.7.0' + implementation 'org.mediawiki:api:1.3' + implementation 'commons-codec:commons-codec:1.10' + implementation 'com.github.pedrovgs:renderers:3.3.3' + implementation 'com.google.code.gson:gson:2.8.1' + implementation 'com.jakewharton.timber:timber:4.5.1' + implementation 'info.debatty:java-string-similarity:0.24' + implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.2.1@aar'){ transitive=true } - compile "com.android.support:support-v4:${project.supportLibVersion}" - compile "com.android.support:appcompat-v7:${project.supportLibVersion}" - compile "com.android.support:design:${project.supportLibVersion}" - compile "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" - annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" + implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION" + implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION" + implementation "com.android.support:design:$SUPPORT_LIB_VERSION" - compile 'com.squareup.okhttp3:okhttp:3.8.1' - compile 'com.squareup.okio:okio:1.13.0' + implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION" - compile 'io.reactivex.rxjava2:rxandroid:2.0.1' + implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" + kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" + + implementation 'com.squareup.okhttp3:okhttp:3.8.1' + implementation 'com.squareup.okio:okio:1.13.0' + + implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' // Because RxAndroid releases are few and far between, it is recommended you also // explicitly depend on RxJava's latest version for bug fixes and new features. - compile 'io.reactivex.rxjava2:rxjava:2.1.2' - compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0' - compile 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0' - compile 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0' - compile 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0' + implementation 'io.reactivex.rxjava2:rxjava:2.1.2' + implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' + implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0' + implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0' + implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0' - compile 'com.facebook.fresco:fresco:1.3.0' - compile 'com.facebook.stetho:stetho:1.5.0' + implementation 'com.facebook.fresco:fresco:1.5.0' + implementation 'com.facebook.stetho:stetho:1.5.0' - testCompile 'junit:junit:4.12' - testCompile 'org.robolectric:robolectric:3.3.2' + implementation "com.google.dagger:dagger:$DAGGER_VERSION" + implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" - testCompile 'com.squareup.okhttp3:mockwebserver:3.8.1' - androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.8.1' - androidTestCompile "com.android.support:support-annotations:${project.supportLibVersion}" - androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' + kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" + kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" - debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1' - releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' - testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' + testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + + testImplementation 'junit:junit:4.12' + testImplementation 'org.robolectric:robolectric:3.4' + testImplementation 'org.mockito:mockito-all:1.10.19' + + testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' + androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' + androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION" + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + + debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY" + releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" + testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" + + implementation "com.google.dagger:dagger:$DAGGER_VERSION" + implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" + kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" + kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" } android { @@ -63,24 +83,38 @@ android { defaultConfig { applicationId 'fr.free.nrw.commons' - versionCode 74 - versionName '2.5.0' + versionCode 82 + versionName '2.6.7' + setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) + minSdkVersion project.minSdkVersion targetSdkVersion project.targetSdkVersion testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } + sourceSets { + // use kotlin only in tests (for now) + test.java.srcDirs += 'src/test/kotlin' + + // use main assets and resources in test + test.assets.srcDirs += 'src/main/assets' + test.resources.srcDirs += 'src/main/resoures' + } + buildTypes { release { minifyEnabled false // See https://stackoverflow.com/questions/40232404/google-play-apk-and-android-studio-apk-usb-debug-behaving-differently - proguard.cfg modification alone insufficient. proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' } debug { + applicationIdSuffix ".debug" testCoverageEnabled true + versionNameSuffix "-debug-" + getBranchName() + "~" + getBuildVersion() } } + flavorDimensions 'tier' productFlavors { prod { buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\"" @@ -92,6 +126,7 @@ android { buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\"" buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\"" + dimension 'tier' } beta { @@ -105,6 +140,7 @@ android { buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\"" buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\"" + dimension 'tier' } } @@ -122,5 +158,8 @@ android { //FIXME: Temporary fix for https://github.com/commons-app/apps-android-commons/issues/709 configurations.all { resolutionStrategy.force 'com.android.support:support-annotations:25.2.0' + exclude module: 'httpclient' + exclude module: 'commons-logging' } + buildToolsVersion buildToolsVersion } diff --git a/app/src/androidTest/java/fr/free/nrw/commons/NearbyActivityTest.java b/app/src/androidTest/java/fr/free/nrw/commons/NearbyActivityTest.java deleted file mode 100644 index f9ec72569..000000000 --- a/app/src/androidTest/java/fr/free/nrw/commons/NearbyActivityTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package fr.free.nrw.commons; - -import android.support.test.espresso.assertion.ViewAssertions; -import android.support.test.filters.LargeTest; -import android.support.test.rule.ActivityTestRule; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import fr.free.nrw.commons.nearby.NearbyActivity; - -import static android.support.test.espresso.Espresso.onView; -import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; -import static android.support.test.espresso.matcher.ViewMatchers.withText; - -@LargeTest -@RunWith(AndroidJUnit4.class) -public class NearbyActivityTest { - @Rule - public final ActivityTestRule nearby = - new ActivityTestRule<>(NearbyActivity.class); - - @Test - public void testActivityLaunch() { - onView(withText(R.string.title_activity_nearby)) - .check(ViewAssertions.matches(isDisplayed())); - } -} diff --git a/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java b/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java new file mode 100644 index 000000000..d2db4614f --- /dev/null +++ b/app/src/androidTest/java/fr/free/nrw/commons/upload/FileUtilsTest.java @@ -0,0 +1,30 @@ +package fr.free.nrw.commons.upload; + +import android.net.Uri; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import fr.free.nrw.commons.BuildConfig; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +@RunWith(AndroidJUnit4.class) +public class FileUtilsTest { + @Test + public void isSelfOwned() throws Exception { + Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".provider/document/1"); + boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri); + assertThat(selfOwned, is(true)); + } + + @Test + public void isNotSelfOwned() throws Exception { + Uri uri = Uri.parse("content://com.android.providers.media.documents/document/1"); + boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri); + assertThat(selfOwned, is(false)); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 882e1fb13..253bdaea8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,24 +2,28 @@ package="fr.free.nrw.commons"> - - - - - + + + + + - - - - - - + + + + + + + + + + - + - - + + - - + + + + android:label="@string/app_name"> @@ -51,11 +51,11 @@ + + android:name=".upload.MultipleShareActivity" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name"> @@ -65,33 +65,38 @@ - + android:name=".contributions.ContributionsActivity" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" /> + + android:label="@string/title_activity_settings" /> + + + android:label="@string/title_activity_signup" /> + - - + + + + + android:process=":auth"> @@ -102,27 +107,25 @@ + android:name=".contributions.ContributionsSyncService" + android:exported="true"> - + + android:name="android.content.SyncAdapter" + android:resource="@xml/contributions_sync_adapter" /> + android:name=".modifications.ModificationsSyncService" + android:exported="true"> - + + android:name="android.content.SyncAdapter" + android:resource="@xml/modifications_sync_adapter" /> + android:resource="@xml/provider_paths" /> - + android:name=".contributions.ContributionsContentProvider" + android:authorities="fr.free.nrw.commons.contributions.contentprovider" + android:exported="false" + android:label="@string/provider_contributions" + android:syncable="true" /> - + android:name=".modifications.ModificationsContentProvider" + android:authorities="fr.free.nrw.commons.modifications.contentprovider" + android:exported="false" + android:label="@string/provider_modifications" + android:syncable="true" /> + - + android:name=".category.CategoryContentProvider" + android:authorities="fr.free.nrw.commons.categories.contentprovider" + android:exported="false" + android:label="@string/provider_categories" + android:syncable="false" /> diff --git a/app/src/main/assets/mapstyle.json b/app/src/main/assets/mapstyle.json new file mode 100644 index 000000000..d4291cbdc --- /dev/null +++ b/app/src/main/assets/mapstyle.json @@ -0,0 +1,26 @@ +{ + "version": 8, + "sources": { + "wikimedia-osm": { + "type": "raster", + "tiles": [ + "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png" + ], + "tileSize": 128 + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#606060" + } + }, + { + "id": "osm", + "type": "raster", + "source": "wikimedia-osm" + } + ] +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java index 96c1cf200..a2f67a3bf 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java @@ -1,19 +1,29 @@ package fr.free.nrw.commons; -import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; +import android.view.View; import android.widget.TextView; import butterknife.BindView; import butterknife.ButterKnife; +import butterknife.OnClick; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.ui.widget.HtmlTextView; +/** + * Represents about screen of this app + */ public class AboutActivity extends NavigationBaseActivity { @BindView(R.id.about_version) TextView versionText; @BindView(R.id.about_license) HtmlTextView aboutLicenseText; + /** + * This method helps in the creation About screen + * + * @param savedInstanceState Data bundle + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -21,15 +31,38 @@ public class AboutActivity extends NavigationBaseActivity { ButterKnife.bind(this); - String aboutText = getString(R.string.about_license, getString(R.string.trademarked_name)); + String aboutText = getString(R.string.about_license); aboutLicenseText.setHtmlText(aboutText); versionText.setText(BuildConfig.VERSION_NAME); initDrawer(); } - public static void startYourself(Context context) { - Intent settingsIntent = new Intent(context, AboutActivity.class); - context.startActivity(settingsIntent); + @OnClick(R.id.facebook_launch_icon) + public void launchFacebook(View view) { + + Intent intent; + try { + intent = new Intent(Intent.ACTION_VIEW, Uri.parse("fb://page/" + "1921335171459985")); + intent.setPackage("com.facebook.katana"); + startActivity(intent); + } catch (Exception e) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.facebook.com/" + "1921335171459985"))); + } + + } + + @OnClick(R.id.github_launch_icon) + public void launchGithub(View view) { + + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/commons-app/apps-android-commons\\")); + startActivity(browserIntent); + } + + @OnClick(R.id.website_launch_icon) + public void launchWebsite(View view) { + + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://commons-app.github.io/\\")); + startActivity(browserIntent); } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 62f5ec1a9..ab156bab7 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -1,40 +1,34 @@ package fr.free.nrw.commons; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerCallback; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; import android.app.Application; import android.content.Context; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.database.sqlite.SQLiteDatabase; -import android.preference.PreferenceManager; -import android.support.v4.util.LruCache; import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.stetho.Stetho; import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.RefWatcher; import org.acra.ACRA; import org.acra.ReportingInteractionMode; import org.acra.annotation.ReportsCrashes; import java.io.File; -import java.io.IOException; -import fr.free.nrw.commons.auth.AccountUtil; -import fr.free.nrw.commons.caching.CacheController; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.data.Category; +import javax.inject.Inject; +import javax.inject.Named; + +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.category.CategoryDao; +import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.modifications.ModifierSequence; -import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi; -import fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.nearby.NearbyPlaces; +import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.di.CommonsApplicationComponent; +import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.utils.FileUtils; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; import timber.log.Timber; // TODO: Use ProGuard to rip out reporting when publishing @@ -48,86 +42,43 @@ import timber.log.Timber; ) public class CommonsApplication extends Application { - private Account currentAccount = null; // Unlike a savings account... + @Inject SessionManager sessionManager; + @Inject DBOpenHelper dbOpenHelper; - public static final Object[] EVENT_UPLOAD_ATTEMPT = {"MobileAppUploadAttempts", 5334329L}; - public static final Object[] EVENT_LOGIN_ATTEMPT = {"MobileAppLoginAttempts", 5257721L}; - public static final Object[] EVENT_SHARE_ATTEMPT = {"MobileAppShareAttempts", 5346170L}; - public static final Object[] EVENT_CATEGORIZATION_ATTEMPT = {"MobileAppCategorizationAttempts", 5359208L}; + @Inject @Named("default_preferences") SharedPreferences defaultPrefs; + @Inject @Named("application_preferences") SharedPreferences applicationPrefs; + @Inject @Named("prefs") SharedPreferences otherPrefs; public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app"; public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com"; + + public static final String LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com"; + public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback"; - private static CommonsApplication instance = null; - private MediaWikiApi api = null; - private LruCache thumbnailUrlCache = new LruCache<>(1024); - private CacheController cacheData = null; - private DBOpenHelper dbOpenHelper = null; - private NearbyPlaces nearbyPlaces = null; + private RefWatcher refWatcher; + /** - * This should not be called by ANY application code (other than the magic Android glue) - * Use CommonsApplication.getInstance() instead to get the singleton. + * Used to declare and initialize various components and dependencies */ - public CommonsApplication() { - CommonsApplication.instance = this; - } - - public static CommonsApplication getInstance() { - if (instance == null) { - instance = new CommonsApplication(); - } - return instance; - } - - public MediaWikiApi getMWApi() { - if (api == null) { - api = new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST); - } - return api; - } - - public CacheController getCacheData() { - if (cacheData == null) { - cacheData = new CacheController(); - } - return cacheData; - } - - public LruCache getThumbnailUrlCache() { - return thumbnailUrlCache; - } - - public synchronized DBOpenHelper getDBOpenHelper() { - if (dbOpenHelper == null) { - dbOpenHelper = new DBOpenHelper(this); - } - return dbOpenHelper; - } - - public synchronized NearbyPlaces getNearbyPlaces() { - if (nearbyPlaces == null) { - nearbyPlaces = new NearbyPlaces(); - } - return nearbyPlaces; - } - @Override public void onCreate() { super.onCreate(); - if (LeakCanary.isInAnalyzerProcess(this)) { - // This process is dedicated to LeakCanary for heap analysis. - // You should not init your app in this process. + + ApplicationlessInjection + .getInstance(this) + .getCommonsApplicationComponent() + .inject(this); + + Fresco.initialize(this); + if (setupLeakCanary() == RefWatcher.DISABLED) { return; } - LeakCanary.install(this); Timber.plant(new Timber.DebugTree()); - - if (!BuildConfig.DEBUG) { ACRA.init(this); } else { @@ -136,52 +87,36 @@ public class CommonsApplication extends Application { // Fire progress callbacks for every 3% of uploaded content System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0"); + } - Fresco.initialize(this); - //For caching area -> categories - cacheData = new CacheController(); + /** + * Helps in setting up LeakCanary library + * @return instance of LeakCanary + */ + protected RefWatcher setupLeakCanary() { + if (LeakCanary.isInAnalyzerProcess(this)) { + return RefWatcher.DISABLED; + } + return LeakCanary.install(this); + } + + /** + * Provides a way to get member refWatcher + * + * @param context Application context + * @return application member refWatcher + */ + public static RefWatcher getRefWatcher(Context context) { + CommonsApplication application = (CommonsApplication) context.getApplicationContext(); + return application.refWatcher; } /** - * @return Account|null + * clears data of current application + * @param context Application context + * @param logoutListener Implementation of interface LogoutListener */ - public Account getCurrentAccount() { - if (currentAccount == null) { - AccountManager accountManager = AccountManager.get(this); - Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType()); - if (allAccounts.length != 0) { - currentAccount = allAccounts[0]; - } - } - return currentAccount; - } - - public Boolean revalidateAuthToken() { - AccountManager accountManager = AccountManager.get(this); - Account curAccount = getCurrentAccount(); - - if (curAccount == null) { - return false; // This should never happen - } - - accountManager.invalidateAuthToken(AccountUtil.accountType(), getMWApi().getAuthCookie()); - try { - String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false); - getMWApi().setAuthCookie(authCookie); - return true; - } catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) { - e.printStackTrace(); - return false; - } - } - - public boolean deviceHasCamera() { - PackageManager pm = getPackageManager(); - return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) || - pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); - } - public void clearApplicationData(Context context, LogoutListener logoutListener) { File cacheDirectory = context.getCacheDir(); File applicationDirectory = new File(cacheDirectory.getParent()); @@ -194,70 +129,37 @@ public class CommonsApplication extends Application { } } - AccountManager accountManager = AccountManager.get(this); - Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.accountType()); - - AccountManagerCallback amCallback = new AccountManagerCallback() { - - private int index = 0; - - void setIndex(int index) { - this.index = index; - } - - int getIndex() { - return index; - } - - @Override - public void run(AccountManagerFuture accountManagerFuture) { - setIndex(getIndex() + 1); - - try { - if (accountManagerFuture != null && accountManagerFuture.getResult()) { - Timber.d("Account removed successfully."); - } - } catch (OperationCanceledException | IOException | AuthenticatorException e) { - e.printStackTrace(); - } - - if (getIndex() == allAccounts.length) { + sessionManager.clearAllAccounts() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { Timber.d("All accounts have been removed"); //TODO: fix preference manager - PreferenceManager.getDefaultSharedPreferences(getInstance()) - .edit().clear().commit(); - SharedPreferences preferences = context - .getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE); - preferences.edit().clear().commit(); - context.getSharedPreferences("prefs", Context.MODE_PRIVATE) - .edit().clear().commit(); - preferences.edit().putBoolean("firstrun", false).apply(); + defaultPrefs.edit().clear().apply(); + applicationPrefs.edit().clear().apply(); + applicationPrefs.edit().putBoolean("firstrun", false).apply(); + otherPrefs.edit().clear().apply(); updateAllDatabases(); - currentAccount = null; logoutListener.onLogoutComplete(); - } - } - }; - - for (Account account : allAccounts) { - accountManager.removeAccount(account, amCallback, null); - } + }); } /** * Deletes all tables and re-creates them. */ - public void updateAllDatabases() { - DBOpenHelper dbOpenHelper = CommonsApplication.getInstance().getDBOpenHelper(); + private void updateAllDatabases() { dbOpenHelper.getReadableDatabase().close(); SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); - ModifierSequence.Table.onDelete(db); - Category.Table.onDelete(db); - Contribution.Table.onDelete(db); + ModifierSequenceDao.Table.onDelete(db); + CategoryDao.Table.onDelete(db); + ContributionDao.Table.onDelete(db); } + /** + * Interface used to get log-out events + */ public interface LogoutListener { void onLogoutComplete(); } diff --git a/app/src/main/java/fr/free/nrw/commons/HandlerService.java b/app/src/main/java/fr/free/nrw/commons/HandlerService.java index 61fa1f1c5..e5e1b3b1b 100644 --- a/app/src/main/java/fr/free/nrw/commons/HandlerService.java +++ b/app/src/main/java/fr/free/nrw/commons/HandlerService.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons; -import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.Handler; @@ -9,7 +8,9 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; -public abstract class HandlerService extends Service { +import fr.free.nrw.commons.di.CommonsDaggerService; + +public abstract class HandlerService extends CommonsDaggerService { private volatile Looper threadLooper; private volatile ServiceHandler threadHandler; private String serviceName; diff --git a/app/src/main/java/fr/free/nrw/commons/License.java b/app/src/main/java/fr/free/nrw/commons/License.java index 797e0f50c..db893de16 100644 --- a/app/src/main/java/fr/free/nrw/commons/License.java +++ b/app/src/main/java/fr/free/nrw/commons/License.java @@ -2,12 +2,25 @@ package fr.free.nrw.commons; import android.support.annotation.Nullable; +/** + * represents Licence object + */ public class License { private String key; private String template; private String url; private String name; + /** + * Constructs a new instance of License. + * + * @param key license key + * @param template license template + * @param url license URL + * @param name licence name + * + * @throws RuntimeException if License.key or Licence.template is null + */ public License(String key, String template, String url, String name) { if (key == null) { throw new RuntimeException("License.key must not be null"); @@ -21,10 +34,18 @@ public class License { this.name = name; } + /** + * Gets the license key. + * @return license key as a String. + */ public String getKey() { return key; } + /** + * Gets the license template. + * @return license template as a String. + */ public String getTemplate() { return template; } @@ -38,6 +59,12 @@ public class License { } } + /** + * Gets the license URL + * + * @param language license language + * @return URL + */ public @Nullable String getUrl(String language) { if (url == null) { return null; diff --git a/app/src/main/java/fr/free/nrw/commons/LicenseList.java b/app/src/main/java/fr/free/nrw/commons/LicenseList.java index 382ceee3f..d08e314cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/LicenseList.java +++ b/app/src/main/java/fr/free/nrw/commons/LicenseList.java @@ -5,21 +5,31 @@ import android.content.res.Resources; import android.support.annotation.Nullable; import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.Locale; import java.util.Map; +/** + * Represents a list of Licenses + */ public class LicenseList { private Map licenses = new HashMap<>(); private Resources res; + /** + * Constructs new instance of LicenceList + * + * @param activity License activity + */ public LicenseList(Activity activity) { res = activity.getResources(); XmlPullParser parser = res.getXml(R.xml.wikimedia_licenses); String namespace = "https://www.mediawiki.org/wiki/Extension:UploadWizard/xmlns/licenses"; - while (Utils.xmlFastForward(parser, namespace, "license")) { + while (xmlFastForward(parser, namespace, "license")) { String id = parser.getAttributeValue(null, "id"); String template = parser.getAttributeValue(null, "template"); String url = parser.getAttributeValue(null, "url"); @@ -29,14 +39,28 @@ public class LicenseList { } } + /** + * Gets a collection of licenses + * @return License values + */ public Collection values() { return licenses.values(); } + /** + * Gets license + * @param key License key + * @return License that matches key + */ public License get(String key) { return licenses.get(key); } + /** + * Creates a license from template + * @param template License template + * @return null + */ @Nullable License licenseForTemplate(String template) { String ucTemplate = new PageTitle(template).getDisplayText(); @@ -48,6 +72,11 @@ public class LicenseList { return null; } + /** + * Gets template name id + * @param template License template + * @return name id of template + */ private String nameIdForTemplate(String template) { // hack :D (converts dashes and periods to underscores) // cc-by-sa-3.0 -> cc_by_sa_3_0 @@ -55,9 +84,44 @@ public class LicenseList { "_").replace(".", "_"); } + /** + * Gets name of given template + * @param template License template + * @return name of template + */ private String nameForTemplate(String template) { int nameId = res.getIdentifier("fr.free.nrw.commons:string/" + nameIdForTemplate(template), null, null); return (nameId != 0) ? res.getString(nameId) : template; } -} \ No newline at end of file + + /** + * Fast-forward an XmlPullParser to the next instance of the given element + * in the input stream (namespaced). + * + * @param parser + * @param namespace + * @param element + * @return true on match, false on failure + */ + private boolean xmlFastForward(XmlPullParser parser, String namespace, String element) { + try { + while (parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.getEventType() == XmlPullParser.START_TAG && + parser.getNamespace().equals(namespace) && + parser.getName().equals(element)) { + // We found it! + return true; + } + } + return false; + } catch (XmlPullParserException e) { + e.printStackTrace(); + return false; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 26a1d038b..726d787f3 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -47,16 +47,35 @@ public class Media implements Parcelable { private HashMap tags = new HashMap<>(); private @Nullable LatLng coordinates; + /** + * Provides local constructor + */ protected Media() { this.categories = new ArrayList<>(); this.descriptions = new HashMap<>(); } + /** + * Provides a minimal constructor + * + * @param filename Media filename + */ public Media(String filename) { this(); this.filename = filename; } + /** + * Provide Media constructor + * @param localUri Media URI + * @param imageUrl Media image URL + * @param filename Media filename + * @param description Media description + * @param dataLength Media date length + * @param dateCreated Media creation date + * @param dateUploaded Media date uploaded + * @param creator Media creator + */ public Media(Uri localUri, String imageUrl, String filename, String description, long dataLength, Date dateCreated, @Nullable Date dateUploaded, String creator) { this(); @@ -90,19 +109,33 @@ public class Media implements Parcelable { descriptions = in.readHashMap(ClassLoader.getSystemClassLoader()); } + /** + * Gets tag of media + * @param key Media key + * @return Media tag + */ public Object getTag(String key) { return tags.get(key); } + /** + * Modifies( or creates a) tag of media + * @param key Media key + * @param value Media value + */ public void setTag(String key, Object value) { tags.put(key, value); } + /** + * Gets media display title + * @return Media title + */ public String getDisplayTitle() { if (filename == null) { return ""; } - // FIXME: Gross hack bercause my regex skills suck maybe or I am too lazy who knows + // FIXME: Gross hack because my regex skills suck maybe or I am too lazy who knows String title = getFilePageTitle().getDisplayText().replaceFirst("^File:", ""); Matcher matcher = displayTitlePattern.matcher(title); if (matcher.matches()) { @@ -112,109 +145,215 @@ public class Media implements Parcelable { } } + /** + * Gets file page title + * @return New media page title + */ public PageTitle getFilePageTitle() { return new PageTitle("File:" + getFilename().replaceFirst("^File:", "")); } + /** + * Gets local URI + * @return Media local URI + */ public Uri getLocalUri() { return localUri; } + /** + * Gets image URL + * can be null. + * @return Image URL + */ + @Nullable public String getImageUrl() { - if (imageUrl == null) { + if (imageUrl == null && this.getFilename() != null) { imageUrl = Utils.makeThumbBaseUrl(this.getFilename()); } return imageUrl; } + /** + * Gets the name of the file. + * @return file name as a string + */ public String getFilename() { return filename; } + /** + * Sets the name of the file. + * @param filename the new name of the file + */ public void setFilename(String filename) { this.filename = filename; } + /** + * Gets the file description. + * @return file description as a string + */ public String getDescription() { return description; } + /** + * Sets the file description. + * @param description the new description of the file + */ public void setDescription(String description) { this.description = description; } + /** + * Gets the datalength of the file. + * @return file datalength as a long + */ public long getDataLength() { return dataLength; } + /** + * Sets the datalength of the file. + * @param dataLength as a long + */ public void setDataLength(long dataLength) { this.dataLength = dataLength; } + /** + * Gets the creation date of the file. + * @return creation date as a Date + */ public Date getDateCreated() { return dateCreated; } + /** + * Sets the creation date of the file. + * @param date creation date as a Date + */ public void setDateCreated(Date date) { this.dateCreated = date; } + /** + * Gets the upload date of the file. + * Can be null. + * @return upload date as a Date + */ public @Nullable Date getDateUploaded() { return dateUploaded; } + /** + * Gets the name of the creator of the file. + * @return creator name as a String + */ public String getCreator() { return creator; } + /** + * Sets the creator name of the file. + * @param creator creator name as a string + */ public void setCreator(String creator) { this.creator = creator; } + /** + * Gets the width of the media. + * @return file width as an int + */ public int getWidth() { return width; } + /** + * Sets the width of the media. + * @param width file width as an int + */ public void setWidth(int width) { this.width = width; } + /** + * Gets the height of the media. + * @return file height as an int + */ public int getHeight() { return height; } + /** + * Sets the height of the media. + * @param height file height as an int + */ public void setHeight(int height) { this.height = height; } + /** + * Gets the license name of the file. + * @return license as a String + */ public String getLicense() { return license; } + /** + * Sets the license name of the file. + * @param license license name as a String + */ public void setLicense(String license) { this.license = license; } + /** + * Gets the coordinates of where the file was created. + * @return file coordinates as a LatLng + */ public @Nullable LatLng getCoordinates() { return coordinates; } + /** + * Sets the coordinates of where the file was created. + * @param coordinates file coordinates as a LatLng + */ public void setCoordinates(@Nullable LatLng coordinates) { this.coordinates = coordinates; } + /** + * Gets the categories the file falls under. + * @return file categories as an ArrayList of Strings + */ @SuppressWarnings("unchecked") public ArrayList getCategories() { return (ArrayList) categories.clone(); // feels dirty } + /** + * Sets the categories the file falls under. + *

+ * Does not append: i.e. will clear the current categories + * and then add the specified ones. + * @param categories file categories as a list of Strings + */ public void setCategories(List categories) { this.categories.clear(); this.categories.addAll(categories); } + /** + * Modifies (or sets) media descriptions + * @param descriptions Media descriptions + */ void setDescriptions(Map descriptions) { for (String key : this.descriptions.keySet()) { this.descriptions.remove(key); @@ -224,6 +363,11 @@ public class Media implements Parcelable { } } + /** + * Gets media description in preferred language + * @param preferredLanguage Language preferred + * @return Description in preferred language + */ public String getDescription(String preferredLanguage) { if (descriptions.containsKey(preferredLanguage)) { // See if the requested language is there. @@ -240,11 +384,21 @@ public class Media implements Parcelable { } } + /** + * Method of Parcelable interface + * @return zero + */ @Override public int describeContents() { return 0; } + /** + * Creates a way to transfer information between two or more + * activities. + * @param parcel Instance of Parcel + * @param flags Parcel flag + */ @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeParcelable(localUri, flags); diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index 5e8ad7386..25e778b74 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -11,12 +11,12 @@ import org.xml.sax.SAXException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.inject.Inject; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -33,46 +33,39 @@ import timber.log.Timber; * which are not intrinsic to the media and may change due to editing. */ public class MediaDataExtractor { + private final MediaWikiApi mediaWikiApi; private boolean fetched; - - private String filename; private ArrayList categories; private Map descriptions; - private Date date; private String license; private @Nullable LatLng coordinates; - private LicenseList licenseList; - /** - * @param filename of the target media object, should include 'File:' prefix - */ - public MediaDataExtractor(String filename, LicenseList licenseList) { - this.filename = filename; - categories = new ArrayList<>(); - descriptions = new HashMap<>(); - fetched = false; - this.licenseList = licenseList; + @Inject + public MediaDataExtractor(MediaWikiApi mwApi) { + this.categories = new ArrayList<>(); + this.descriptions = new HashMap<>(); + this.fetched = false; + this.mediaWikiApi = mwApi; } - /** + /* * Actually fetch the data over the network. * todo: use local caching? * * Warning: synchronous i/o, call on a background thread */ - public void fetch() throws IOException { + public void fetch(String filename, LicenseList licenseList) throws IOException { if (fetched) { throw new IllegalStateException("Tried to call MediaDataExtractor.fetch() again."); } - MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); - MediaResult result = api.fetchMediaByFilename(filename); + MediaResult result = mediaWikiApi.fetchMediaByFilename(filename); // In-page category links are extracted from source, as XML doesn't cover [[links]] extractCategories(result.getWikiSource()); // Description template info is extracted from preprocessor XML - processWikiParseTree(result.getParseTreeXmlSource()); + processWikiParseTree(result.getParseTreeXmlSource(), licenseList); fetched = true; } @@ -91,7 +84,7 @@ public class MediaDataExtractor { } } - private void processWikiParseTree(String source) throws IOException { + private void processWikiParseTree(String source, LicenseList licenseList) throws IOException { Document doc; try { DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); @@ -153,7 +146,7 @@ public class MediaDataExtractor { } private Node findTemplate(Element parentNode, String title_) throws IOException { - String title= new PageTitle(title_).getDisplayText(); + String title = new PageTitle(title_).getDisplayText(); NodeList nodes = parentNode.getChildNodes(); for (int i = 0, length = nodes.getLength(); i < length; i++) { Node node = nodes.item(i); @@ -179,7 +172,7 @@ public class MediaDataExtractor { } private static abstract class TemplateChildNodeComparator { - abstract public boolean match(Node node); + public abstract boolean match(Node node); } private Node findTemplateParameter(Node templateNode, String name) throws IOException { @@ -290,7 +283,7 @@ public class MediaDataExtractor { /** * Take our metadata and inject it into a live Media object. * Media object might contain stale or cached data, or emptiness. - * @param media + * @param media Media object to inject into */ public void fill(Media media) { if (!fetched) { diff --git a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java b/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java index 5ccc80c06..a542cb363 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaThumbnailFetchTask.java @@ -7,16 +7,17 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi; class MediaThumbnailFetchTask extends AsyncTask { protected final Media media; + private MediaWikiApi mediaWikiApi; - public MediaThumbnailFetchTask(@NonNull Media media) { + public MediaThumbnailFetchTask(@NonNull Media media, MediaWikiApi mwApi) { this.media = media; + this.mediaWikiApi = mwApi; } @Override protected String doInBackground(String... params) { try { - MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); - return api.findThumbnailByFilename(params[0]); + return mediaWikiApi.findThumbnailByFilename(params[0]); } catch (Exception e) { // Do something better! } diff --git a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java index 3e147f4a8..35d197782 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java @@ -4,6 +4,7 @@ import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.graphics.drawable.VectorDrawableCompat; +import android.support.v4.util.LruCache; import android.text.TextUtils; import android.util.AttributeSet; import android.widget.Toast; @@ -11,9 +12,16 @@ import android.widget.Toast; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.view.SimpleDraweeView; +import javax.inject.Inject; + +import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; public class MediaWikiImageView extends SimpleDraweeView { + @Inject MediaWikiApi mwApi; + @Inject LruCache thumbnailUrlCache; + private ThumbnailFetchTask currentThumbnailTask; public MediaWikiImageView(Context context) { @@ -31,19 +39,23 @@ public class MediaWikiImageView extends SimpleDraweeView { init(); } + /** + * Sets the media. Fetches its thumbnail if necessary. + * @param media the new media + */ public void setMedia(Media media) { if (currentThumbnailTask != null) { currentThumbnailTask.cancel(true); } - if(media == null) { + if (media == null) { return; } - if (CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename()) != null) { - setImageUrl(CommonsApplication.getInstance().getThumbnailUrlCache().get(media.getFilename())); + if (thumbnailUrlCache.get(media.getFilename()) != null) { + setImageUrl(thumbnailUrlCache.get(media.getFilename())); } else { setImageUrl(null); - currentThumbnailTask = new ThumbnailFetchTask(media); + currentThumbnailTask = new ThumbnailFetchTask(media, mwApi); currentThumbnailTask.execute(media.getFilename()); } } @@ -56,7 +68,15 @@ public class MediaWikiImageView extends SimpleDraweeView { super.onDetachedFromWindow(); } + /** + * Initializes MediaWikiImageView. + */ private void init() { + ApplicationlessInjection + .getInstance(getContext() + .getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); setHierarchy(GenericDraweeHierarchyBuilder .newInstance(getResources()) .setPlaceholderImage(VectorDrawableCompat.create(getResources(), @@ -66,13 +86,17 @@ public class MediaWikiImageView extends SimpleDraweeView { .build()); } + /** + * Displays the image from the URL. + * @param url the URL of the image + */ private void setImageUrl(@Nullable String url) { setImageURI(url); } private class ThumbnailFetchTask extends MediaThumbnailFetchTask { - ThumbnailFetchTask(@NonNull Media media) { - super(media); + ThumbnailFetchTask(@NonNull Media media, @NonNull MediaWikiApi mwApi) { + super(media, mwApi); } @Override @@ -85,7 +109,7 @@ public class MediaWikiImageView extends SimpleDraweeView { } else { // only cache meaningful thumbnails received from network. try { - CommonsApplication.getInstance().getThumbnailUrlCache().put(media.getFilename(), result); + thumbnailUrlCache.put(media.getFilename(), result); } catch (NullPointerException npe) { Timber.e("error when adding pic to cache " + npe); diff --git a/app/src/main/java/fr/free/nrw/commons/PageTitle.java b/app/src/main/java/fr/free/nrw/commons/PageTitle.java index 6229e7ef9..c4c52b1fc 100644 --- a/app/src/main/java/fr/free/nrw/commons/PageTitle.java +++ b/app/src/main/java/fr/free/nrw/commons/PageTitle.java @@ -84,6 +84,12 @@ public class PageTitle { return titleKey; } + /** + * Gets the canonicalized title for displaying (such as "File:My example.jpg"). + *

+ * Essentially equivalent to getPrefixedText + * @return canonical title as a String + */ @Override public String toString() { return getPrefixedText(); diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 0ccdaf724..817c39f24 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -1,99 +1,26 @@ package fr.free.nrw.commons; import android.content.Context; -import android.os.Build; import android.preference.PreferenceManager; -import android.text.Html; -import android.text.Spanned; +import android.support.annotation.NonNull; import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; -import org.w3c.dom.Node; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; -import java.io.StringWriter; +import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; -import java.math.BigInteger; import java.net.URLEncoder; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.Locale; -import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.TransformerFactoryConfigurationError; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; - import fr.free.nrw.commons.settings.Prefs; import timber.log.Timber; public class Utils { - // Get SHA1 of file from input stream - public static String getSHA1(InputStream is) { - - MessageDigest digest; - try { - digest = MessageDigest.getInstance("SHA1"); - } catch (NoSuchAlgorithmException e) { - Timber.e(e, "Exception while getting Digest"); - return ""; - } - - byte[] buffer = new byte[8192]; - int read; - try { - while ((read = is.read(buffer)) > 0) { - digest.update(buffer, 0, read); - } - byte[] md5sum = digest.digest(); - BigInteger bigInt = new BigInteger(1, md5sum); - String output = bigInt.toString(16); - // Fill to 40 chars - output = String.format("%40s", output).replace(' ', '0'); - Timber.i("File SHA1: %s", output); - - return output; - } catch (IOException e) { - Timber.e(e, "IO Exception"); - return ""; - } finally { - try { - is.close(); - } catch (IOException e) { - Timber.e(e, "Exception on closing MD5 input stream"); - } - } - } - - /** - * Fix Html.fromHtml is deprecated problem - * - * @param source provided Html string - * @return returned Spanned of appropriate method according to version check - */ - public static Spanned fromHtml(String source) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY); - } else { - //noinspection deprecation - return Html.fromHtml(source); - } - } - /** * Strips localization symbols from a string. * Removes the suffix after "@" and quotes. @@ -110,49 +37,23 @@ public class Utils { } } - public static Date parseMWDate(String mwDate) { - SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC - try { - return isoFormat.parse(mwDate); - } catch (ParseException e) { - throw new RuntimeException(e); - } - } - - public static String toMWDate(Date date) { - SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC - isoFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - return isoFormat.format(date); - } - - public static String makeThumbBaseUrl(String filename) { + /** + * Creates an URL for thumbnail + * + * @param filename Thumbnail file name + * @return URL of thumbnail + */ + public static String makeThumbBaseUrl(@NonNull String filename) { String name = new PageTitle(filename).getPrefixedText(); String sha = new String(Hex.encodeHex(DigestUtils.md5(name))); return String.format("%s/%s/%s/%s", BuildConfig.IMAGE_URL_BASE, sha.substring(0, 1), sha.substring(0, 2), urlEncode(name)); } - public static String getStringFromDOM(Node dom) { - Transformer transformer = null; - try { - transformer = TransformerFactory.newInstance().newTransformer(); - } catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - StringWriter outputStream = new StringWriter(); - DOMSource domSource = new DOMSource(dom); - StreamResult strResult = new StreamResult(outputStream); - - try { - transformer.transform(domSource, strResult); - } catch (TransformerException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - return outputStream.toString(); - } - + /** + * URL Encode an URL in UTF-8 format + * @param url Unformatted URL + * @return Encoded URL + */ public static String urlEncode(String url) { try { return URLEncoder.encode(url, "utf-8"); @@ -161,39 +62,21 @@ public class Utils { } } - public static long countBytes(InputStream stream) throws IOException { - long count = 0; - BufferedInputStream bis = new BufferedInputStream(stream); - while (bis.read() != -1) { - count++; - } - return count; - } - + /** + * Capitalizes the first character of a string. + * + * @param string String to alter + * @return string with capitalized first character + */ public static String capitalize(String string) { return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1); } - public static String licenseTemplateFor(String license) { - switch (license) { - case Prefs.Licenses.CC_BY_3: - return "{{self|cc-by-3.0}}"; - case Prefs.Licenses.CC_BY_4: - return "{{self|cc-by-4.0}}"; - case Prefs.Licenses.CC_BY_SA_3: - return "{{self|cc-by-sa-3.0}}"; - case Prefs.Licenses.CC_BY_SA_4: - return "{{self|cc-by-sa-4.0}}"; - case Prefs.Licenses.CC0: - return "{{self|cc-zero}}"; - case Prefs.Licenses.CC_BY: - return "{{self|cc-by-3.0}}"; - case Prefs.Licenses.CC_BY_SA: - return "{{self|cc-by-sa-3.0}}"; - } - throw new RuntimeException("Unrecognized license value: " + license); - } - + /** + * Generates licence name with given ID + * @param license License ID + * @return Name of license + */ public static int licenseNameFor(String license) { switch (license) { case Prefs.Licenses.CC_BY_3: @@ -214,51 +97,12 @@ public class Utils { throw new RuntimeException("Unrecognized license value: " + license); } - public static String licenseUrlFor(String license) { - switch (license) { - case Prefs.Licenses.CC_BY_3: - return "https://creativecommons.org/licenses/by/3.0/"; - case Prefs.Licenses.CC_BY_4: - return "https://creativecommons.org/licenses/by/4.0/"; - case Prefs.Licenses.CC_BY_SA_3: - return "https://creativecommons.org/licenses/by-sa/3.0/"; - case Prefs.Licenses.CC_BY_SA_4: - return "https://creativecommons.org/licenses/by-sa/4.0/"; - case Prefs.Licenses.CC0: - return "https://creativecommons.org/publicdomain/zero/1.0/"; - } - throw new RuntimeException("Unrecognized license value: " + license); - } - /** - * Fast-forward an XmlPullParser to the next instance of the given element - * in the input stream (namespaced). - * - * @param parser - * @param namespace - * @param element - * @return true on match, false on failure + * Fixing incorrect extension + * @param title File name + * @param extension Correct extension + * @return File with correct extension */ - public static boolean xmlFastForward(XmlPullParser parser, String namespace, String element) { - try { - while (parser.next() != XmlPullParser.END_DOCUMENT) { - if (parser.getEventType() == XmlPullParser.START_TAG - && parser.getNamespace().equals(namespace) - && parser.getName().equals(element)) { - // We found it! - return true; - } - } - return false; - } catch (XmlPullParserException e) { - e.printStackTrace(); - return false; - } catch (IOException e) { - e.printStackTrace(); - return false; - } - } - public static String fixExtension(String title, String extension) { Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE); @@ -274,11 +118,45 @@ public class Utils { return title; } - public static boolean isNullOrWhiteSpace(String value) { - return value == null || value.trim().isEmpty(); - } - + /** + * Tells whether dark theme is active or not + * @param context Activity context + * @return The state of dark theme + */ public static boolean isDarkTheme(Context context) { return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme", false); } + + /** + * Will be used to fetch the logs generated by the app ever since the beginning of times.... + * i.e. since the time the app started. + * + * @return String containing all the logs since the time the app started + */ + public static String getAppLogs() { + final String processId = Integer.toString(android.os.Process.myPid()); + + StringBuilder stringBuilder = new StringBuilder(); + + try { + String[] command = new String[] {"logcat","-d","-v","threadtime"}; + + Process process = Runtime.getRuntime().exec(command); + + BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(process.getInputStream()) + ); + + String line; + while ((line = bufferedReader.readLine()) != null) { + if (line.contains(processId)) { + stringBuilder.append(line); + } + } + } catch (IOException ioe) { + Timber.e("getAppLogs failed", ioe); + } + + return stringBuilder.toString(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java index f6c5999e9..7bfb22890 100644 --- a/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/WelcomeActivity.java @@ -18,6 +18,11 @@ public class WelcomeActivity extends BaseActivity { private WelcomePagerAdapter adapter = new WelcomePagerAdapter(); + /** + * Initialises exiting fields and dependencies + * + * @param savedInstanceState WelcomeActivity bundled data + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -30,12 +35,20 @@ public class WelcomeActivity extends BaseActivity { adapter.setCallback(this::finish); } + /** + * References WelcomePageAdapter to null before the activity is destroyed + */ @Override public void onDestroy() { adapter.setCallback(null); super.onDestroy(); } + /** + * Creates a way to change current activity to WelcomeActivity + * + * @param context Activity context + */ public static void startYourself(Context context) { Intent welcomeIntent = new Intent(context, WelcomeActivity.class); context.startActivity(welcomeIntent); diff --git a/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java index 9669196e9..a346655cf 100644 --- a/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java @@ -10,13 +10,6 @@ import butterknife.ButterKnife; import butterknife.OnClick; public class WelcomePagerAdapter extends PagerAdapter { - private static final int PAGE_FINAL = 4; - private Callback callback; - - public interface Callback { - void onYesClicked(); - } - static final int[] PAGE_LAYOUTS = new int[]{ R.layout.welcome_wikipedia, R.layout.welcome_do_upload, @@ -24,16 +17,34 @@ public class WelcomePagerAdapter extends PagerAdapter { R.layout.welcome_image_details, R.layout.welcome_final }; + private static final int PAGE_FINAL = 4; + private Callback callback; + /** + * Changes callback to provided one + * + * @param callback New callback + * it can be null. + */ public void setCallback(@Nullable Callback callback) { this.callback = callback; } + /** + * Gets total number of layouts + * @return Number of layouts + */ @Override public int getCount() { return PAGE_LAYOUTS.length; } + /** + * Compares given view with provided object + * @param view Adapter view + * @param object Adapter object + * @return Equality between view and object + */ @Override public boolean isViewFromObject(View view, Object object) { return (view == object); @@ -52,16 +63,29 @@ public class WelcomePagerAdapter extends PagerAdapter { return layout; } + /** + * Provides a way to remove an item from container + * @param container Adapter view group container + * @param position Index of item + * @param obj Adapter object + */ @Override public void destroyItem(ViewGroup container, int position, Object obj) { container.removeView((View) obj); } + public interface Callback { + void onYesClicked(); + } + class ViewHolder { ViewHolder(View view) { ButterKnife.bind(this, view); } + /** + * Triggers on click callback on button click + */ @OnClick(R.id.welcomeYesButton) void onClicked() { if (callback != null) { diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java index 479b47444..0513280b5 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java @@ -4,21 +4,33 @@ import android.accounts.Account; import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountManager; import android.content.ContentResolver; +import android.content.Context; import android.os.Bundle; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.contributions.ContributionsContentProvider; -import fr.free.nrw.commons.modifications.ModificationsContentProvider; import timber.log.Timber; +import static android.accounts.AccountManager.ERROR_CODE_REMOTE_EXCEPTION; +import static android.accounts.AccountManager.KEY_ACCOUNT_NAME; +import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE; +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY; +import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MODIFICATIONS_AUTHORITY; + public class AccountUtil { - public static void createAccount(@Nullable AccountAuthenticatorResponse response, - String username, String password) { + public static final String ACCOUNT_TYPE = "fr.free.nrw.commons"; + public static final String AUTH_COOKIE = "authCookie"; + public static final String AUTH_TOKEN_TYPE = "CommonsAndroid"; + private final Context context; - Account account = new Account(username, accountType()); + public AccountUtil(Context context) { + this.context = context; + } + + public void createAccount(@Nullable AccountAuthenticatorResponse response, + String username, String password) { + + Account account = new Account(username, ACCOUNT_TYPE); boolean created = accountManager().addAccountExplicitly(account, password, null); Timber.d("account creation " + (created ? "successful" : "failure")); @@ -26,8 +38,8 @@ public class AccountUtil { if (created) { if (response != null) { Bundle bundle = new Bundle(); - bundle.putString(AccountManager.KEY_ACCOUNT_NAME, username); - bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType()); + bundle.putString(KEY_ACCOUNT_NAME, username); + bundle.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); response.onResult(bundle); @@ -35,28 +47,18 @@ public class AccountUtil { } else { if (response != null) { - response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, ""); + response.onError(ERROR_CODE_REMOTE_EXCEPTION, ""); } Timber.d("account creation failure"); } // FIXME: If the user turns it off, it shouldn't be auto turned back on - ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default! - ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default! + ContentResolver.setSyncAutomatically(account, CONTRIBUTION_AUTHORITY, true); // Enable sync by default! + ContentResolver.setSyncAutomatically(account, MODIFICATIONS_AUTHORITY, true); // Enable sync by default! } - @NonNull - public static String accountType() { - return "fr.free.nrw.commons"; - } - - private static AccountManager accountManager() { - return AccountManager.get(app()); - } - - @NonNull - private static CommonsApplication app() { - return CommonsApplication.getInstance(); + private AccountManager accountManager() { + return AccountManager.get(context); } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java index 5b483c328..e39528252 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java @@ -1,84 +1,45 @@ package fr.free.nrw.commons.auth; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerFuture; import android.os.Bundle; -import fr.free.nrw.commons.CommonsApplication; +import javax.inject.Inject; + +import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.theme.NavigationBaseActivity; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; + +import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE; public abstract class AuthenticatedActivity extends NavigationBaseActivity { - private String accountType; - CommonsApplication app; - + @Inject SessionManager sessionManager; + @Inject + MediaWikiApi mediaWikiApi; private String authCookie; - public AuthenticatedActivity() { - this.accountType = AccountUtil.accountType(); - } - - private void getAuthCookie(Account account, AccountManager accountManager) { - Single.fromCallable(() -> accountManager.blockingGetAuthToken(account, "", false)) - .subscribeOn(Schedulers.io()) - .doOnError(Timber::e) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::onAuthCookieAcquired, throwable -> onAuthFailure()); - } - - private void addAccount(AccountManager accountManager) { - Single.just(accountManager.addAccount(accountType, null, null, null, AuthenticatedActivity.this, null, null)) - .subscribeOn(Schedulers.io()) - .map(AccountManagerFuture::getResult) - .doOnEvent((bundle, throwable) -> { - if (!bundle.containsKey(AccountManager.KEY_ACCOUNT_NAME)) { - throw new RuntimeException("Bundle doesn't contain account-name key: " - + AccountManager.KEY_ACCOUNT_NAME); - } - }) - .map(bundle -> bundle.getString(AccountManager.KEY_ACCOUNT_NAME)) - .doOnError(Timber::e) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(s -> { - Account[] allAccounts = accountManager.getAccountsByType(accountType); - Account curAccount = allAccounts[0]; - getAuthCookie(curAccount, accountManager); - }, - throwable -> onAuthFailure()); - } - protected void requestAuthToken() { if (authCookie != null) { onAuthCookieAcquired(authCookie); return; } - AccountManager accountManager = AccountManager.get(this); - Account curAccount = app.getCurrentAccount(); - if (curAccount == null) { - addAccount(accountManager); - } else { - getAuthCookie(curAccount, accountManager); + authCookie = sessionManager.getAuthCookie(); + if (authCookie != null) { + onAuthCookieAcquired(authCookie); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - app = CommonsApplication.getInstance(); + if (savedInstanceState != null) { - authCookie = savedInstanceState.getString("authCookie"); + authCookie = savedInstanceState.getString(AUTH_COOKIE); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putString("authCookie", authCookie); + outState.putString(AUTH_COOKIE, authCookie); } protected abstract void onAuthCookieAcquired(String authCookie); diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 156741865..3e90fbf5e 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -1,115 +1,118 @@ package fr.free.nrw.commons.auth; +import android.accounts.Account; import android.accounts.AccountAuthenticatorActivity; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; import android.app.ProgressDialog; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; +import android.support.annotation.ColorRes; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.support.design.widget.TextInputLayout; import android.support.v4.app.NavUtils; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatDelegate; import android.text.Editable; import android.text.TextWatcher; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; -import android.widget.Toast; +import java.io.IOException; + +import javax.inject.Inject; +import javax.inject.Named; + +import butterknife.BindView; +import butterknife.ButterKnife; import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.WelcomeActivity; - import fr.free.nrw.commons.PageTitle; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.theme.NavigationBaseActivity; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; import timber.log.Timber; import static android.view.KeyEvent.KEYCODE_ENTER; +import static android.view.View.VISIBLE; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; - +import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; +import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; public class LoginActivity extends AccountAuthenticatorActivity { public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username"; - private SharedPreferences prefs = null; + @Inject MediaWikiApi mwApi; + @Inject AccountUtil accountUtil; + @Inject SessionManager sessionManager; + @Inject @Named("application_preferences") SharedPreferences prefs; + @Inject @Named("default_preferences") SharedPreferences defaultPrefs; - private Button loginButton; - private EditText usernameEdit; - private EditText passwordEdit; - private EditText twoFactorEdit; + @BindView(R.id.loginButton) Button loginButton; + @BindView(R.id.signupButton) Button signupButton; + @BindView(R.id.loginUsername) EditText usernameEdit; + @BindView(R.id.loginPassword) EditText passwordEdit; + @BindView(R.id.loginTwoFactor) EditText twoFactorEdit; + @BindView(R.id.error_message_container) ViewGroup errorMessageContainer; + @BindView(R.id.error_message) TextView errorMessage; + @BindView(R.id.two_factor_container)TextInputLayout twoFactorContainer; ProgressDialog progressDialog; + private AppCompatDelegate delegate; private LoginTextWatcher textWatcher = new LoginTextWatcher(); - private CommonsApplication app; - @Override public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme); + getDelegate().installViewFactory(); + getDelegate().onCreate(savedInstanceState); - app = CommonsApplication.getInstance(); + super.onCreate(savedInstanceState); + ApplicationlessInjection + .getInstance(this.getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); setContentView(R.layout.activity_login); - loginButton = (Button) findViewById(R.id.loginButton); - Button signupButton = (Button) findViewById(R.id.signupButton); - usernameEdit = (EditText) findViewById(R.id.loginUsername); - passwordEdit = (EditText) findViewById(R.id.loginPassword); - twoFactorEdit = (EditText) findViewById(R.id.loginTwoFactor); - - prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE); + ButterKnife.bind(this); usernameEdit.addTextChangedListener(textWatcher); passwordEdit.addTextChangedListener(textWatcher); twoFactorEdit.addTextChangedListener(textWatcher); passwordEdit.setOnEditorActionListener(newLoginInputActionListener()); - loginButton.setOnClickListener(this::performLogin); - signupButton.setOnClickListener(this::signUp); + loginButton.setOnClickListener(view -> performLogin()); + signupButton.setOnClickListener(view -> signUp()); } - private class LoginTextWatcher implements TextWatcher { - @Override - public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void afterTextChanged(Editable editable) { - if (usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 && - (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE)) { - loginButton.setEnabled(true); - } else { - loginButton.setEnabled(false); - } - } - } - - private TextView.OnEditorActionListener newLoginInputActionListener() { - return (textView, actionId, keyEvent) -> { - if (loginButton.isEnabled()) { - if (actionId == IME_ACTION_DONE) { - performLogin(textView); - return true; - } else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) { - performLogin(textView); - return true; - } - } - return false; - }; + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + getDelegate().onPostCreate(savedInstanceState); } + @Override protected void onResume() { super.onResume(); if (prefs.getBoolean("firstrun", true)) { WelcomeActivity.startYourself(this); prefs.edit().putBoolean("firstrun", false).apply(); } - if (app.getCurrentAccount() != null) { + if (sessionManager.getCurrentAccount() != null) { startMainActivity(); } } @@ -127,22 +130,113 @@ public class LoginActivity extends AccountAuthenticatorActivity { usernameEdit.removeTextChangedListener(textWatcher); passwordEdit.removeTextChangedListener(textWatcher); twoFactorEdit.removeTextChangedListener(textWatcher); + delegate.onDestroy(); super.onDestroy(); } - private void performLogin(View view) { + private void performLogin() { Timber.d("Login to start!"); - LoginTask task = getLoginTask(); - task.execute(); + final String username = canonicializeUsername(usernameEdit.getText().toString()); + final String password = passwordEdit.getText().toString(); + String twoFactorCode = twoFactorEdit.getText().toString(); + + showLoggingProgressBar(); + Observable.fromCallable(() -> login(username, password, twoFactorCode)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> handleLogin(username, password, result)); } - private LoginTask getLoginTask() { - return new LoginTask( - this, - canonicializeUsername(usernameEdit.getText().toString()), - passwordEdit.getText().toString(), - twoFactorEdit.getText().toString() - ); + private String login(String username, String password, String twoFactorCode) { + try { + if (twoFactorCode.isEmpty()) { + return mwApi.login(username, password); + } else { + return mwApi.login(username, password, twoFactorCode); + } + } catch (IOException e) { + // Do something better! + return "NetworkFailure"; + } + } + + private void handleLogin(String username, String password, String result) { + Timber.d("Login done!"); + if (result.equals("PASS")) { + handlePassResult(username, password); + } else { + handleOtherResults(result); + } + } + + private void showLoggingProgressBar() { + progressDialog = new ProgressDialog(this); + progressDialog.setIndeterminate(true); + progressDialog.setTitle(getString(R.string.logging_in_title)); + progressDialog.setMessage(getString(R.string.logging_in_message)); + progressDialog.setCanceledOnTouchOutside(false); + progressDialog.show(); + } + + private void handlePassResult(String username, String password) { + showSuccessAndDismissDialog(); + requestAuthToken(); + AccountAuthenticatorResponse response = null; + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + Timber.d("Bundle of extras: %s", extras); + response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); + if (response != null) { + Bundle authResult = new Bundle(); + authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username); + authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); + response.onResult(authResult); + } + } + + accountUtil.createAccount(response, username, password); + startMainActivity(); + } + + protected void requestAuthToken() { + AccountManager accountManager = AccountManager.get(this); + Account curAccount = sessionManager.getCurrentAccount(); + if (curAccount != null) { + accountManager.setAuthToken(curAccount, AUTH_TOKEN_TYPE, mwApi.getAuthCookie()); + } + } + + /** + * Match known failure message codes and provide messages. + * + * @param result String + */ + private void handleOtherResults(String result) { + if (result.equals("NetworkFailure")) { + // Matches NetworkFailure which is created by the doInBackground method + showMessageAndCancelDialog(R.string.login_failed_network); + } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { + // Matches nosuchuser, nosuchusershort, noname + showMessageAndCancelDialog(R.string.login_failed_username); + emptySensitiveEditFields(); + } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { + // Matches wrongpassword, wrongpasswordempty + showMessageAndCancelDialog(R.string.login_failed_password); + emptySensitiveEditFields(); + } else if (result.toLowerCase().contains("throttle".toLowerCase())) { + // Matches unknown throttle error codes + showMessageAndCancelDialog(R.string.login_failed_throttled); + } else if (result.toLowerCase().contains("userblocked".toLowerCase())) { + // Matches login-userblocked + showMessageAndCancelDialog(R.string.login_failed_blocked); + } else if (result.equals("2FA")) { + askUserForTwoFactorAuth(); + } else { + // Occurs with unhandled login failure codes + Timber.d("Login failed with reason: %s", result); + showMessageAndCancelDialog(R.string.login_failed_generic); + } } /** @@ -154,6 +248,29 @@ public class LoginActivity extends AccountAuthenticatorActivity { return new PageTitle(username).getText(); } + @Override + protected void onStart() { + super.onStart(); + delegate.onStart(); + } + + @Override + protected void onStop() { + super.onStop(); + delegate.onStop(); + } + + @Override + protected void onPostResume() { + super.onPostResume(); + getDelegate().onPostResume(); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().setContentView(view, params); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -164,36 +281,26 @@ public class LoginActivity extends AccountAuthenticatorActivity { return super.onOptionsItemSelected(item); } - /** - * Called when Sign Up button is clicked. - * @param view View - */ - public void signUp(View view) { - Intent intent = new Intent(this, SignupActivity.class); - startActivity(intent); + @Override + @NonNull + public MenuInflater getMenuInflater() { + return getDelegate().getMenuInflater(); } public void askUserForTwoFactorAuth() { - if (BuildConfig.DEBUG) { - twoFactorEdit.setVisibility(View.VISIBLE); - showUserToastAndCancelDialog(R.string.login_failed_2fa_needed); - } else { - showUserToastAndCancelDialog(R.string.login_failed_2fa_not_supported); - } + progressDialog.dismiss(); + twoFactorContainer.setVisibility(VISIBLE); + twoFactorEdit.setVisibility(VISIBLE); + showMessageAndCancelDialog(R.string.login_failed_2fa_needed); } - public void showUserToastAndCancelDialog(int resId) { - showUserToast(resId); + public void showMessageAndCancelDialog(@StringRes int resId) { + showMessage(resId, R.color.secondaryDarkColor); progressDialog.cancel(); } - private void showUserToast(int resId) { - Toast.makeText(this, resId, Toast.LENGTH_LONG).show(); - } - - public void showSuccessToastAndDismissDialog() { - Toast successToast = Toast.makeText(this, R.string.login_success, Toast.LENGTH_SHORT); - successToast.show(); + public void showSuccessAndDismissDialog() { + showMessage(R.string.login_success, R.color.primaryDarkColor); progressDialog.dismiss(); } @@ -203,8 +310,57 @@ public class LoginActivity extends AccountAuthenticatorActivity { } public void startMainActivity() { - ContributionsActivity.startYourself(this); + NavigationBaseActivity.startActivityWithFlags(this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); finish(); } + private void signUp() { + Intent intent = new Intent(this, SignupActivity.class); + startActivity(intent); + } + + private TextView.OnEditorActionListener newLoginInputActionListener() { + return (textView, actionId, keyEvent) -> { + if (loginButton.isEnabled()) { + if (actionId == IME_ACTION_DONE) { + performLogin(); + return true; + } else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) { + performLogin(); + return true; + } + } + return false; + }; + } + + private void showMessage(@StringRes int resId, @ColorRes int colorResId) { + errorMessage.setText(getString(resId)); + errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); + errorMessageContainer.setVisibility(VISIBLE); + } + + private AppCompatDelegate getDelegate() { + if (delegate == null) { + delegate = AppCompatDelegate.create(this, null); + } + return delegate; + } + + private class LoginTextWatcher implements TextWatcher { + @Override + public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence charSequence, int start, int count, int after) { + } + + @Override + public void afterTextChanged(Editable editable) { + boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 + && (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE); + loginButton.setEnabled(enabled); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java deleted file mode 100644 index dd4f1c4e7..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java +++ /dev/null @@ -1,125 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.AccountAuthenticatorResponse; -import android.accounts.AccountManager; -import android.app.ProgressDialog; -import android.os.AsyncTask; -import android.os.Bundle; - -import java.io.IOException; - -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.mwapi.EventLog; -import timber.log.Timber; - -class LoginTask extends AsyncTask { - - private LoginActivity loginActivity; - private String username; - private String password; - private String twoFactorCode = ""; - private CommonsApplication app; - - public LoginTask(LoginActivity loginActivity, String username, String password, String twoFactorCode) { - this.loginActivity = loginActivity; - this.username = username; - this.password = password; - this.twoFactorCode = twoFactorCode; - app = CommonsApplication.getInstance(); - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - loginActivity.progressDialog = new ProgressDialog(loginActivity); - loginActivity.progressDialog.setIndeterminate(true); - loginActivity.progressDialog.setTitle(loginActivity.getString(R.string.logging_in_title)); - loginActivity.progressDialog.setMessage(loginActivity.getString(R.string.logging_in_message)); - loginActivity.progressDialog.setCanceledOnTouchOutside(false); - loginActivity.progressDialog.show(); - } - - @Override - protected String doInBackground(String... params) { - try { - if (twoFactorCode.isEmpty()) { - return app.getMWApi().login(username, password); - } else { - return app.getMWApi().login(username, password, twoFactorCode); - } - } catch (IOException e) { - // Do something better! - return "NetworkFailure"; - } - } - - @Override - protected void onPostExecute(String result) { - super.onPostExecute(result); - Timber.d("Login done!"); - - EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT) - .param("username", username) - .param("result", result) - .log(); - - if (result.equals("PASS")) { - handlePassResult(); - } else { - handleOtherResults(result); - } - } - - private void handlePassResult() { - loginActivity.showSuccessToastAndDismissDialog(); - - AccountAuthenticatorResponse response = null; - - Bundle extras = loginActivity.getIntent().getExtras(); - if (extras != null) { - Timber.d("Bundle of extras: %s", extras); - response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); - if (response != null) { - Bundle authResult = new Bundle(); - authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username); - authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountUtil.accountType()); - response.onResult(authResult); - } - } - - AccountUtil.createAccount(response, username, password); - loginActivity.startMainActivity(); - } - - /** - * Match known failure message codes and provide messages. - * @param result String - */ - private void handleOtherResults(String result) { - if (result.equals("NetworkFailure")) { - // Matches NetworkFailure which is created by the doInBackground method - loginActivity.showUserToastAndCancelDialog(R.string.login_failed_network); - } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { - // Matches nosuchuser, nosuchusershort, noname - loginActivity.showUserToastAndCancelDialog(R.string.login_failed_username); - loginActivity.emptySensitiveEditFields(); - } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { - // Matches wrongpassword, wrongpasswordempty - loginActivity.showUserToastAndCancelDialog(R.string.login_failed_password); - loginActivity.emptySensitiveEditFields(); - } else if (result.toLowerCase().contains("throttle".toLowerCase())) { - // Matches unknown throttle error codes - loginActivity.showUserToastAndCancelDialog(R.string.login_failed_throttled); - } else if (result.toLowerCase().contains("userblocked".toLowerCase())) { - // Matches login-userblocked - loginActivity.showUserToastAndCancelDialog(R.string.login_failed_blocked); - } else if (result.equals("2FA")) { - loginActivity.askUserForTwoFactorAuth(); - } else { - // Occurs with unhandled login failure codes - Timber.d("Login failed with reason: %s", result); - loginActivity.showUserToastAndCancelDialog(R.string.login_failed_generic); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java new file mode 100644 index 000000000..a7e62c34e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java @@ -0,0 +1,85 @@ +package fr.free.nrw.commons.auth; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.Context; +import android.content.SharedPreferences; + +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import io.reactivex.Completable; +import io.reactivex.Observable; +import timber.log.Timber; + +import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; + +/** + * Manage the current logged in user session. + */ +public class SessionManager { + private final Context context; + private final MediaWikiApi mediaWikiApi; + private Account currentAccount; // Unlike a savings account... ;-) + private SharedPreferences sharedPreferences; + + public SessionManager(Context context, MediaWikiApi mediaWikiApi, SharedPreferences sharedPreferences) { + this.context = context; + this.mediaWikiApi = mediaWikiApi; + this.currentAccount = null; + this.sharedPreferences = sharedPreferences; + } + + /** + * @return Account|null + */ + public Account getCurrentAccount() { + if (currentAccount == null) { + AccountManager accountManager = AccountManager.get(context); + Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE); + if (allAccounts.length != 0) { + currentAccount = allAccounts[0]; + } + } + return currentAccount; + } + + public Boolean revalidateAuthToken() { + AccountManager accountManager = AccountManager.get(context); + Account curAccount = getCurrentAccount(); + + if (curAccount == null) { + return false; // This should never happen + } + + accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie()); + String authCookie = getAuthCookie(); + + if (authCookie == null) { + return false; + } + mediaWikiApi.setAuthCookie(authCookie); + return true; + } + + public String getAuthCookie() { + boolean isLoggedIn = sharedPreferences.getBoolean("isUserLoggedIn", false); + + if (!isLoggedIn) { + Timber.e("User is not logged in"); + return null; + } else { + String authCookie = sharedPreferences.getString("getAuthCookie", null); + if (authCookie == null) { + Timber.e("Auth cookie is null even after login"); + } + return authCookie; + } + } + + public Completable clearAllAccounts() { + AccountManager accountManager = AccountManager.get(context); + Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE); + return Completable.fromObservable(Observable.fromArray(allAccounts) + .map(a -> accountManager.removeAccount(a, null, null).getResult())) + .doOnComplete(() -> currentAccount = null); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java index 4da090531..a6b66cbf6 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java @@ -7,7 +7,6 @@ import android.webkit.WebViewClient; import android.widget.Toast; import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.theme.BaseActivity; import timber.log.Timber; @@ -39,11 +38,8 @@ public class SignupActivity extends BaseActivity { //Signup success, so clear cookies, notify user, and load LoginActivity again Timber.d("Overriding URL %s", url); - Toast toast = Toast.makeText( - CommonsApplication.getInstance(), - "Account created!", - Toast.LENGTH_LONG - ); + Toast toast = Toast.makeText(SignupActivity.this, + "Account created!", Toast.LENGTH_LONG); toast.show(); // terminate on task completion. finish(); diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java index ea574d432..78039f6a9 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java @@ -5,51 +5,37 @@ import android.accounts.Account; import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountManager; import android.accounts.NetworkErrorException; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import java.io.IOException; +import fr.free.nrw.commons.contributions.ContributionsContentProvider; +import fr.free.nrw.commons.modifications.ModificationsContentProvider; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.mwapi.MediaWikiApi; - -import static android.accounts.AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION; -import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE; -import static android.accounts.AccountManager.KEY_ACCOUNT_NAME; -import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE; -import static android.accounts.AccountManager.KEY_AUTHTOKEN; -import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT; -import static android.accounts.AccountManager.KEY_ERROR_CODE; -import static android.accounts.AccountManager.KEY_ERROR_MESSAGE; -import static android.accounts.AccountManager.KEY_INTENT; -import static fr.free.nrw.commons.auth.LoginActivity.PARAM_USERNAME; +import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; +import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { + private static final String[] SYNC_AUTHORITIES = {ContributionsContentProvider.CONTRIBUTION_AUTHORITY, ModificationsContentProvider.MODIFICATIONS_AUTHORITY}; - private Context context; + @NonNull + private final Context context; - WikiAccountAuthenticator(Context context) { + public WikiAccountAuthenticator(@NonNull Context context) { super(context); this.context = context; } - private Bundle unsupportedOperation() { + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { Bundle bundle = new Bundle(); - bundle.putInt(KEY_ERROR_CODE, ERROR_CODE_UNSUPPORTED_OPERATION); - - // HACK: the docs indicate that this is a required key bit it's not displayed to the user. - bundle.putString(KEY_ERROR_MESSAGE, ""); - + bundle.putString("test", "editProperties"); return bundle; } - private boolean supportedAccountType(@Nullable String type) { - return AccountUtil.accountType().equals(type); - } - @Override public Bundle addAccount(@NonNull AccountAuthenticatorResponse response, @NonNull String accountType, @Nullable String authTokenType, @@ -57,87 +43,48 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { throws NetworkErrorException { if (!supportedAccountType(accountType)) { - return unsupportedOperation(); + Bundle bundle = new Bundle(); + bundle.putString("test", "addAccount"); + return bundle; } return addAccount(response); } - private Bundle addAccount(AccountAuthenticatorResponse response) { - Intent Intent = new Intent(context, LoginActivity.class); - Intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); - - Bundle bundle = new Bundle(); - bundle.putParcelable(KEY_INTENT, Intent); - - return bundle; - } - @Override public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response, @NonNull Account account, @Nullable Bundle options) throws NetworkErrorException { - return unsupportedOperation(); + Bundle bundle = new Bundle(); + bundle.putString("test", "confirmCredentials"); + return bundle; } @Override - public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { - return unsupportedOperation(); - } - - private String getAuthCookie(String username, String password) throws IOException { - MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); - //TODO add 2fa support here - String result = api.login(username, password); - if (result.equals("PASS")) { - return api.getAuthCookie(); - } else { - return null; - } - } - - @Override - public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, - String authTokenType, Bundle options) throws NetworkErrorException { - // Extract the username and password from the Account Manager, and ask - // the server for an appropriate AuthToken. - final AccountManager am = AccountManager.get(context); - final String password = am.getPassword(account); - if (password != null) { - String authCookie; - try { - authCookie = getAuthCookie(account.name, password); - } catch (IOException e) { - // Network error! - e.printStackTrace(); - throw new NetworkErrorException(e); - } - if (authCookie != null) { - final Bundle result = new Bundle(); - result.putString(KEY_ACCOUNT_NAME, account.name); - result.putString(KEY_ACCOUNT_TYPE, AccountUtil.accountType()); - result.putString(KEY_AUTHTOKEN, authCookie); - return result; - } - } - - // If we get here, then we couldn't access the user's password - so we - // need to re-prompt them for their credentials. We do that by creating - // an intent to display our AuthenticatorActivity panel. - final Intent intent = new Intent(context, LoginActivity.class); - intent.putExtra(PARAM_USERNAME, account.name); - intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); - final Bundle bundle = new Bundle(); - bundle.putParcelable(KEY_INTENT, intent); + public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response, + @NonNull Account account, @NonNull String authTokenType, + @Nullable Bundle options) + throws NetworkErrorException { + Bundle bundle = new Bundle(); + bundle.putString("test", "getAuthToken"); return bundle; } @Nullable @Override public String getAuthTokenLabel(@NonNull String authTokenType) { - //Note: the wikipedia app actually returns a string here.... - //return supportedAccountType(authTokenType) ? context.getString(R.string.wikimedia) : null; - return null; + return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null; + } + + @Nullable + @Override + public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response, + @NonNull Account account, @Nullable String authTokenType, + @Nullable Bundle options) + throws NetworkErrorException { + Bundle bundle = new Bundle(); + bundle.putString("test", "updateCredentials"); + return bundle; } @Nullable @@ -146,16 +93,50 @@ public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { @NonNull Account account, @NonNull String[] features) throws NetworkErrorException { Bundle bundle = new Bundle(); - bundle.putBoolean(KEY_BOOLEAN_RESULT, false); + bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); return bundle; } - @Nullable - @Override - public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @Nullable String authTokenType, - @Nullable Bundle options) throws NetworkErrorException { - return unsupportedOperation(); + private boolean supportedAccountType(@Nullable String type) { + return ACCOUNT_TYPE.equals(type); } + private Bundle addAccount(AccountAuthenticatorResponse response) { + Intent intent = new Intent(context, LoginActivity.class); + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); + + Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + + return bundle; + } + + private Bundle unsupportedOperation() { + Bundle bundle = new Bundle(); + bundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION); + + // HACK: the docs indicate that this is a required key bit it's not displayed to the user. + bundle.putString(AccountManager.KEY_ERROR_MESSAGE, ""); + + return bundle; + } + + @Override + public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, + Account account) throws NetworkErrorException { + Bundle result = super.getAccountRemovalAllowed(response, account); + + if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) + && !result.containsKey(AccountManager.KEY_INTENT)) { + boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); + + if (allowed) { + for (String auth : SYNC_AUTHORITIES) { + ContentResolver.cancelSync(account, auth); + } + } + } + + return result; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java index 0a996b7d4..826f2ceee 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java @@ -1,24 +1,26 @@ package fr.free.nrw.commons.auth; -import android.accounts.AccountManager; -import android.app.Service; +import android.accounts.AbstractAccountAuthenticator; import android.content.Intent; import android.os.IBinder; +import android.support.annotation.Nullable; -public class WikiAccountAuthenticatorService extends Service { +import fr.free.nrw.commons.di.CommonsDaggerService; + +public class WikiAccountAuthenticatorService extends CommonsDaggerService { + + @Nullable + private AbstractAccountAuthenticator authenticator; - private static WikiAccountAuthenticator wikiAccountAuthenticator = null; - @Override - public IBinder onBind(Intent intent) { - if (!intent.getAction().equals(AccountManager.ACTION_AUTHENTICATOR_INTENT)) { - return null; - } - - if (wikiAccountAuthenticator == null) { - wikiAccountAuthenticator = new WikiAccountAuthenticator(this); - } - return wikiAccountAuthenticator.getIBinder(); + public void onCreate() { + super.onCreate(); + authenticator = new WikiAccountAuthenticator(this); } + @Nullable + @Override + public IBinder onBind(Intent intent) { + return authenticator == null ? null : authenticator.getIBinder(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java index 1a70bc57c..7c2e910c4 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java @@ -1,10 +1,7 @@ package fr.free.nrw.commons.category; -import android.content.ContentProviderClient; import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; -import android.support.v4.app.Fragment; import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -31,11 +28,14 @@ import java.util.HashMap; import java.util.List; import java.util.concurrent.TimeUnit; +import javax.inject.Inject; +import javax.inject.Named; + import butterknife.BindView; import butterknife.ButterKnife; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.data.Category; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.upload.MwVolleyApi; import fr.free.nrw.commons.utils.StringSortingUtils; import io.reactivex.Observable; @@ -45,12 +45,11 @@ import timber.log.Timber; import static android.view.KeyEvent.ACTION_UP; import static android.view.KeyEvent.KEYCODE_BACK; -import static fr.free.nrw.commons.category.CategoryContentProvider.AUTHORITY; /** * Displays the category suggestion and selection screen. Category search is initiated here. */ -public class CategorizationFragment extends Fragment { +public class CategorizationFragment extends CommonsDaggerSupportFragment { public static final int SEARCH_CATS_LIMIT = 25; @@ -65,16 +64,19 @@ public class CategorizationFragment extends Fragment { @BindView(R.id.categoriesExplanation) TextView categoriesSkip; + @Inject MediaWikiApi mwApi; + @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject CategoryDao categoryDao; + private RVRendererAdapter categoriesAdapter; private OnCategoriesSaveHandler onCategoriesSaveHandler; private HashMap> categoriesCache; private List selectedCategories = new ArrayList<>(); - private ContentProviderClient databaseClient; private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> { if (item.isSelected()) { selectedCategories.add(item); - updateCategoryCount(item, databaseClient); + updateCategoryCount(item); } else { selectedCategories.remove(item); } @@ -88,13 +90,6 @@ public class CategorizationFragment extends Fragment { categoriesList.setLayoutManager(new LinearLayoutManager(getContext())); - RxView.clicks(categoriesSkip) - .takeUntil(RxView.detaches(categoriesSkip)) - .subscribe(o -> { - getActivity().onBackPressed(); - getActivity().finish(); - }); - ArrayList items = new ArrayList<>(); categoriesCache = new HashMap<>(); if (savedInstanceState != null) { @@ -139,12 +134,6 @@ public class CategorizationFragment extends Fragment { } } - @Override - public void onDestroy() { - super.onDestroy(); - databaseClient.release(); - } - @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -180,7 +169,6 @@ public class CategorizationFragment extends Fragment { setHasOptionsMenu(true); onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity(); getActivity().setTitle(R.string.categories_activity_title); - databaseClient = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY); } private void updateCategoryList(String filter) { @@ -205,7 +193,9 @@ public class CategorizationFragment extends Fragment { .sorted(sortBySimilarity(filter)) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - s -> categoriesAdapter.add(s), Timber::e, () -> { + s -> categoriesAdapter.add(s), + Timber::e, + () -> { categoriesAdapter.notifyDataSetChanged(); categoriesSearchInProgress.setVisibility(View.GONE); @@ -253,16 +243,15 @@ public class CategorizationFragment extends Fragment { private Observable titleCategories() { //Retrieve the title that was saved when user tapped submit icon - SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity()); - String title = titleDesc.getString("Title", ""); + String title = prefs.getString("Title", ""); - return CommonsApplication.getInstance().getMWApi() + return mwApi .searchTitles(title, SEARCH_CATS_LIMIT) .map(name -> new CategoryItem(name, false)); } private Observable recentCategories() { - return Observable.fromIterable(Category.recentCategories(databaseClient, SEARCH_CATS_LIMIT)) + return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT)) .map(s -> new CategoryItem(s, false)); } @@ -279,7 +268,7 @@ public class CategorizationFragment extends Fragment { } //otherwise, search API for matching categories - return CommonsApplication.getInstance().getMWApi() + return mwApi .allCategories(term, SEARCH_CATS_LIMIT) .map(name -> new CategoryItem(name, false)); } @@ -290,7 +279,7 @@ public class CategorizationFragment extends Fragment { return Observable.empty(); } - return CommonsApplication.getInstance().getMWApi() + return mwApi .searchCategories(term, SEARCH_CATS_LIMIT) .map(s -> new CategoryItem(s, false)); } @@ -308,28 +297,22 @@ public class CategorizationFragment extends Fragment { //Check if item contains a 4-digit word anywhere within the string (.* is wildcard) //And that item does not equal the current year or previous year //And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750) + //Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029 return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString)) - || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")); + || item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)") + || (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*"))); } - private void updateCategoryCount(CategoryItem item, ContentProviderClient client) { - Category cat = lookupCategory(item.getName()); - cat.incTimesUsed(); - cat.save(client); - } + private void updateCategoryCount(CategoryItem item) { + Category category = categoryDao.find(item.getName()); - private Category lookupCategory(String name) { - Category cat = Category.find(databaseClient, name); - - if (cat == null) { - // Newly used category... - cat = new Category(); - cat.setName(name); - cat.setLastUsed(new Date()); - cat.setTimesUsed(0); + // Newly used category... + if (category == null) { + category = new Category(null, item.getName(), new Date(), 0); } - return cat; + category.incTimesUsed(); + categoryDao.save(category); } public int getCurrentSelectedCount() { diff --git a/app/src/main/java/fr/free/nrw/commons/category/Category.java b/app/src/main/java/fr/free/nrw/commons/category/Category.java new file mode 100644 index 000000000..f2d83d2e5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/Category.java @@ -0,0 +1,96 @@ +package fr.free.nrw.commons.category; + +import android.net.Uri; + +import java.util.Date; + +/** + * Represents a category + */ +public class Category { + private Uri contentUri; + private String name; + private Date lastUsed; + private int timesUsed; + + public Category() { + } + + public Category(Uri contentUri, String name, Date lastUsed, int timesUsed) { + this.contentUri = contentUri; + this.name = name; + this.lastUsed = lastUsed; + this.timesUsed = timesUsed; + } + + /** + * Gets name + * + * @return name + */ + public String getName() { + return name; + } + + /** + * Modifies name + * + * @param name Category name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets last used date + * + * @return Last used date + */ + public Date getLastUsed() { + // warning: Date objects are mutable. + return (Date)lastUsed.clone(); + } + + /** + * Generates new last used date + */ + private void touch() { + lastUsed = new Date(); + } + + /** + * Gets no. of times the category is used + * + * @return no. of times used + */ + public int getTimesUsed() { + return timesUsed; + } + + /** + * Increments timesUsed by 1 and sets last used date as now. + */ + public void incTimesUsed() { + timesUsed++; + touch(); + } + + /** + * Gets the content URI for this category + * + * @return content URI + */ + public Uri getContentUri() { + return contentUri; + } + + /** + * Modifies the content URI - marking this category as already saved in the database + * + * @param contentUri the content URI + */ + public void setContentUri(Uri contentUri) { + this.contentUri = contentUri; + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java index 9fd125b7d..16cf49742 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons.category; -import android.content.ContentProvider; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; @@ -10,17 +9,18 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.text.TextUtils; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.data.Category; +import javax.inject.Inject; + import fr.free.nrw.commons.data.DBOpenHelper; +import fr.free.nrw.commons.di.CommonsDaggerContentProvider; import timber.log.Timber; import static android.content.UriMatcher.NO_MATCH; -import static fr.free.nrw.commons.data.Category.Table.ALL_FIELDS; -import static fr.free.nrw.commons.data.Category.Table.COLUMN_ID; -import static fr.free.nrw.commons.data.Category.Table.TABLE_NAME; +import static fr.free.nrw.commons.category.CategoryDao.Table.ALL_FIELDS; +import static fr.free.nrw.commons.category.CategoryDao.Table.COLUMN_ID; +import static fr.free.nrw.commons.category.CategoryDao.Table.TABLE_NAME; -public class CategoryContentProvider extends ContentProvider { +public class CategoryContentProvider extends CommonsDaggerContentProvider { public static final String AUTHORITY = "fr.free.nrw.commons.categories.contentprovider"; // For URI matcher @@ -37,19 +37,11 @@ public class CategoryContentProvider extends ContentProvider { uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CATEGORIES_ID); } - private DBOpenHelper dbOpenHelper; - public static Uri uriForId(int id) { return Uri.parse(BASE_URI.toString() + "/" + id); } - @SuppressWarnings("ConstantConditions") - @Override - public boolean onCreate() { - CommonsApplication app = ((CommonsApplication) getContext().getApplicationContext()); - dbOpenHelper = app.getDBOpenHelper(); - return false; - } + @Inject DBOpenHelper dbOpenHelper; @SuppressWarnings("ConstantConditions") @Override diff --git a/app/src/main/java/fr/free/nrw/commons/data/Category.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java similarity index 54% rename from app/src/main/java/fr/free/nrw/commons/data/Category.java rename to app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java index 757f6b691..e63b04c26 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/Category.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java @@ -1,115 +1,64 @@ -package fr.free.nrw.commons.data; +package fr.free.nrw.commons.category; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; import android.os.RemoteException; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.util.ArrayList; import java.util.Date; +import java.util.List; -import fr.free.nrw.commons.category.CategoryContentProvider; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; -public class Category { - private Uri contentUri; +public class CategoryDao { - private String name; - private Date lastUsed; - private int timesUsed; + private final Provider clientProvider; - // Getters/setters - public String getName() { - return name; + @Inject + public CategoryDao(@Named("category") Provider clientProvider) { + this.clientProvider = clientProvider; } - public void setName(String name) { - this.name = name; - } - - private Date getLastUsed() { - // warning: Date objects are mutable. - return (Date)lastUsed.clone(); - } - - public void setLastUsed(Date lastUsed) { - // warning: Date objects are mutable. - this.lastUsed = (Date)lastUsed.clone(); - } - - private void touch() { - lastUsed = new Date(); - } - - private int getTimesUsed() { - return timesUsed; - } - - public void setTimesUsed(int timesUsed) { - this.timesUsed = timesUsed; - } - - public void incTimesUsed() { - timesUsed++; - touch(); - } - - //region Database/content-provider stuff - - /** - * Persist category. - * @param client ContentProviderClient to handle DB connection - */ - public void save(ContentProviderClient client) { + public void save(Category category) { + ContentProviderClient db = clientProvider.get(); try { - if (contentUri == null) { - contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues()); + if (category.getContentUri() == null) { + category.setContentUri(db.insert(CategoryContentProvider.BASE_URI, toContentValues(category))); } else { - client.update(contentUri, toContentValues(), null, null); + db.update(category.getContentUri(), toContentValues(category), null, null); } } catch (RemoteException e) { throw new RuntimeException(e); + } finally { + db.release(); } } - private ContentValues toContentValues() { - ContentValues cv = new ContentValues(); - cv.put(Table.COLUMN_NAME, getName()); - cv.put(Table.COLUMN_LAST_USED, getLastUsed().getTime()); - cv.put(Table.COLUMN_TIMES_USED, getTimesUsed()); - return cv; - } - - private static Category fromCursor(Cursor cursor) { - // Hardcoding column positions! - Category c = new Category(); - c.contentUri = CategoryContentProvider.uriForId(cursor.getInt(0)); - c.name = cursor.getString(1); - c.lastUsed = new Date(cursor.getLong(2)); - c.timesUsed = cursor.getInt(3); - return c; - } - /** * Find persisted category in database, based on its name. - * @param client ContentProviderClient to handle DB connection + * * @param name Category's name * @return category from database, or null if not found */ - public static @Nullable Category find(ContentProviderClient client, String name) { + @Nullable + Category find(String name) { Cursor cursor = null; + ContentProviderClient db = clientProvider.get(); try { - cursor = client.query( + cursor = db.query( CategoryContentProvider.BASE_URI, - Category.Table.ALL_FIELDS, - Category.Table.COLUMN_NAME + "=?", + Table.ALL_FIELDS, + Table.COLUMN_NAME + "=?", new String[]{name}, null); if (cursor != null && cursor.moveToFirst()) { - return Category.fromCursor(cursor); + return fromCursor(cursor); } } catch (RemoteException e) { // This feels lazy, but to hell with checked exceptions. :) @@ -118,29 +67,32 @@ public class Category { if (cursor != null) { cursor.close(); } + db.release(); } return null; } /** * Retrieve recently-used categories, ordered by descending date. + * * @return a list containing recent categories */ - public static @NonNull ArrayList recentCategories(ContentProviderClient client, int limit) { - ArrayList items = new ArrayList<>(); + @NonNull + List recentCategories(int limit) { + List items = new ArrayList<>(); Cursor cursor = null; + ContentProviderClient db = clientProvider.get(); try { - cursor = client.query( + cursor = db.query( CategoryContentProvider.BASE_URI, - Category.Table.ALL_FIELDS, + Table.ALL_FIELDS, null, new String[]{}, - Category.Table.COLUMN_LAST_USED + " DESC"); + Table.COLUMN_LAST_USED + " DESC"); // fixme add a limit on the original query instead of falling out of the loop? while (cursor != null && cursor.moveToNext() && cursor.getPosition() < limit) { - Category cat = Category.fromCursor(cursor); - items.add(cat.getName()); + items.add(fromCursor(cursor).getName()); } } catch (RemoteException e) { throw new RuntimeException(e); @@ -148,17 +100,36 @@ public class Category { if (cursor != null) { cursor.close(); } + db.release(); } return items; } + Category fromCursor(Cursor cursor) { + // Hardcoding column positions! + return new Category( + CategoryContentProvider.uriForId(cursor.getInt(0)), + cursor.getString(1), + new Date(cursor.getLong(2)), + cursor.getInt(3) + ); + } + + private ContentValues toContentValues(Category category) { + ContentValues cv = new ContentValues(); + cv.put(CategoryDao.Table.COLUMN_NAME, category.getName()); + cv.put(CategoryDao.Table.COLUMN_LAST_USED, category.getLastUsed().getTime()); + cv.put(CategoryDao.Table.COLUMN_TIMES_USED, category.getTimesUsed()); + return cv; + } + public static class Table { public static final String TABLE_NAME = "categories"; public static final String COLUMN_ID = "_id"; - public static final String COLUMN_NAME = "name"; - public static final String COLUMN_LAST_USED = "last_used"; - public static final String COLUMN_TIMES_USED = "times_used"; + static final String COLUMN_NAME = "name"; + static final String COLUMN_LAST_USED = "last_used"; + static final String COLUMN_TIMES_USED = "times_used"; // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. public static final String[] ALL_FIELDS = { @@ -168,7 +139,9 @@ public class Category { COLUMN_TIMES_USED }; - private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" + static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; + + static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" + COLUMN_ID + " INTEGER PRIMARY KEY," + COLUMN_NAME + " STRING," + COLUMN_LAST_USED + " INTEGER," @@ -180,7 +153,7 @@ public class Category { } public static void onDelete(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + db.execSQL(DROP_TABLE_STATEMENT); onCreate(db); } @@ -208,5 +181,4 @@ public class Category { } } } - //endregion } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index f9060b7b5..7861f96de 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -1,13 +1,8 @@ package fr.free.nrw.commons.contributions; -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.Parcel; -import android.os.RemoteException; -import android.text.TextUtils; +import android.support.annotation.NonNull; import java.text.SimpleDateFormat; import java.util.Date; @@ -16,7 +11,6 @@ import java.util.Locale; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.settings.Prefs; public class Contribution extends Media { @@ -43,7 +37,6 @@ public class Contribution extends Media { public static final String SOURCE_GALLERY = "gallery"; public static final String SOURCE_EXTERNAL = "external"; - private ContentProviderClient client; private Uri contentUri; private String source; private String editSummary; @@ -51,24 +44,42 @@ public class Contribution extends Media { private int state; private long transferred; private String decimalCoords; - private boolean isMultiple; - public boolean getMultiple() { - return isMultiple; + public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp, + int state, long dataLength, Date dateUploaded, long transferred, + String source, String description, String creator, boolean isMultiple, + int width, int height, String license) { + super(localUri, imageUrl, filename, description, dataLength, timestamp, dateUploaded, creator); + this.contentUri = contentUri; + this.state = state; + this.timestamp = timestamp; + this.transferred = transferred; + this.source = source; + this.isMultiple = isMultiple; + this.width = width; + this.height = height; + this.license = license; } - public void setMultiple(boolean multiple) { - isMultiple = multiple; - } - - public Contribution(Uri localUri, String remoteUri, String filename, String description, long dataLength, Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) { - super(localUri, remoteUri, filename, description, dataLength, dateCreated, dateUploaded, creator); + public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength, + Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) { + super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator); this.decimalCoords = decimalCoords; this.editSummary = editSummary; timestamp = new Date(System.currentTimeMillis()); } + public Contribution(Parcel in) { + super(in); + contentUri = in.readParcelable(Uri.class.getClassLoader()); + source = in.readString(); + timestamp = (Date) in.readSerializable(); + state = in.readInt(); + transferred = in.readLong(); + isMultiple = in.readInt() == 1; + } + @Override public void writeToParcel(Parcel parcel, int flags) { super.writeToParcel(parcel, flags); @@ -80,14 +91,12 @@ public class Contribution extends Media { parcel.writeInt(isMultiple ? 1 : 0); } - public Contribution(Parcel in) { - super(in); - contentUri = in.readParcelable(Uri.class.getClassLoader()); - source = in.readString(); - timestamp = (Date) in.readSerializable(); - state = in.readInt(); - transferred = in.readLong(); - isMultiple = in.readInt() == 1; + public boolean getMultiple() { + return isMultiple; + } + + public void setMultiple(boolean multiple) { + isMultiple = multiple; } public long getTransferred() { @@ -106,10 +115,18 @@ public class Contribution extends Media { return contentUri; } + public void setContentUri(Uri contentUri) { + this.contentUri = contentUri; + } + public Date getTimestamp() { return timestamp; } + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + public int getState() { return state; } @@ -149,68 +166,12 @@ public class Contribution extends Media { } buffer.append("== {{int:license-header}} ==\n") - .append(Utils.licenseTemplateFor(getLicense())).append("\n\n") + .append(licenseTemplateFor(getLicense())).append("\n\n") .append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n") .append(getTrackingTemplates()); return buffer.toString(); } - public void setContentProviderClient(ContentProviderClient client) { - this.client = client; - } - - public void save() { - try { - if (contentUri == null) { - contentUri = client.insert(ContributionsContentProvider.BASE_URI, this.toContentValues()); - } else { - client.update(contentUri, toContentValues(), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - public void delete() { - try { - if (contentUri == null) { - // noooo - throw new RuntimeException("tried to delete item with no content URI"); - } else { - client.delete(contentUri, null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - - public ContentValues toContentValues() { - ContentValues cv = new ContentValues(); - cv.put(Table.COLUMN_FILENAME, getFilename()); - if (getLocalUri() != null) { - cv.put(Table.COLUMN_LOCAL_URI, getLocalUri().toString()); - } - if (getImageUrl() != null) { - cv.put(Table.COLUMN_IMAGE_URL, getImageUrl()); - } - if (getDateUploaded() != null) { - cv.put(Table.COLUMN_UPLOADED, getDateUploaded().getTime()); - } - cv.put(Table.COLUMN_LENGTH, getDataLength()); - cv.put(Table.COLUMN_TIMESTAMP, getTimestamp().getTime()); - cv.put(Table.COLUMN_STATE, getState()); - cv.put(Table.COLUMN_TRANSFERRED, transferred); - cv.put(Table.COLUMN_SOURCE, source); - cv.put(Table.COLUMN_DESCRIPTION, description); - cv.put(Table.COLUMN_CREATOR, creator); - cv.put(Table.COLUMN_MULTIPLE, isMultiple ? 1 : 0); - cv.put(Table.COLUMN_WIDTH, width); - cv.put(Table.COLUMN_HEIGHT, height); - cv.put(Table.COLUMN_LICENSE, license); - return cv; - } - @Override public void setFilename(String filename) { this.filename = filename; @@ -224,33 +185,6 @@ public class Contribution extends Media { timestamp = new Date(System.currentTimeMillis()); } - public static Contribution fromCursor(Cursor cursor) { - // Hardcoding column positions! - Contribution c = new Contribution(); - - //Check that cursor has a value to avoid CursorIndexOutOfBoundsException - if (cursor.getCount() > 0) { - c.contentUri = ContributionsContentProvider.uriForId(cursor.getInt(0)); - c.filename = cursor.getString(1); - c.localUri = TextUtils.isEmpty(cursor.getString(2)) ? null : Uri.parse(cursor.getString(2)); - c.imageUrl = cursor.getString(3); - c.timestamp = cursor.getLong(4) == 0 ? null : new Date(cursor.getLong(4)); - c.state = cursor.getInt(5); - c.dataLength = cursor.getLong(6); - c.dateUploaded = cursor.getLong(7) == 0 ? null : new Date(cursor.getLong(7)); - c.transferred = cursor.getLong(8); - c.source = cursor.getString(9); - c.description = cursor.getString(10); - c.creator = cursor.getString(11); - c.isMultiple = cursor.getInt(12) == 1; - c.width = cursor.getInt(13); - c.height = cursor.getInt(14); - c.license = cursor.getString(15); - } - - return c; - } - public String getSource() { return source; } @@ -267,118 +201,25 @@ public class Contribution extends Media { this.decimalCoords = decimalCoords; } - public static class Table { - public static final String TABLE_NAME = "contributions"; - - public static final String COLUMN_ID = "_id"; - public static final String COLUMN_FILENAME = "filename"; - public static final String COLUMN_LOCAL_URI = "local_uri"; - public static final String COLUMN_IMAGE_URL = "image_url"; - public static final String COLUMN_TIMESTAMP = "timestamp"; - public static final String COLUMN_STATE = "state"; - public static final String COLUMN_LENGTH = "length"; - public static final String COLUMN_UPLOADED = "uploaded"; - public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes - public static final String COLUMN_SOURCE = "source"; - public static final String COLUMN_DESCRIPTION = "description"; - public static final String COLUMN_CREATOR = "creator"; // Initial uploader - public static final String COLUMN_MULTIPLE = "multiple"; - public static final String COLUMN_WIDTH = "width"; - public static final String COLUMN_HEIGHT = "height"; - public static final String COLUMN_LICENSE = "license"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_ID, - COLUMN_FILENAME, - COLUMN_LOCAL_URI, - COLUMN_IMAGE_URL, - COLUMN_TIMESTAMP, - COLUMN_STATE, - COLUMN_LENGTH, - COLUMN_UPLOADED, - COLUMN_TRANSFERRED, - COLUMN_SOURCE, - COLUMN_DESCRIPTION, - COLUMN_CREATOR, - COLUMN_MULTIPLE, - COLUMN_WIDTH, - COLUMN_HEIGHT, - COLUMN_LICENSE - }; - - - private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + "_id INTEGER PRIMARY KEY," - + "filename STRING," - + "local_uri STRING," - + "image_url STRING," - + "uploaded INTEGER," - + "timestamp INTEGER," - + "state INTEGER," - + "length INTEGER," - + "transferred INTEGER," - + "source STRING," - + "description STRING," - + "creator STRING," - + "multiple INTEGER," - + "width INTEGER," - + "height INTEGER," - + "LICENSE STRING" - + ");"; - - - public static void onCreate(SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); + @NonNull + private String licenseTemplateFor(String license) { + switch (license) { + case Prefs.Licenses.CC_BY_3: + return "{{self|cc-by-3.0}}"; + case Prefs.Licenses.CC_BY_4: + return "{{self|cc-by-4.0}}"; + case Prefs.Licenses.CC_BY_SA_3: + return "{{self|cc-by-sa-3.0}}"; + case Prefs.Licenses.CC_BY_SA_4: + return "{{self|cc-by-sa-4.0}}"; + case Prefs.Licenses.CC0: + return "{{self|cc-zero}}"; + case Prefs.Licenses.CC_BY: + return "{{self|cc-by-3.0}}"; + case Prefs.Licenses.CC_BY_SA: + return "{{self|cc-by-sa-3.0}}"; } - public static void onDelete(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); - onCreate(db); - } - - public static void onUpdate(SQLiteDatabase db, int from, int to) { - if (from == to) { - return; - } - if (from == 1) { - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;"); - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;"); - from++; - onUpdate(db, from, to); - return; - } - if (from == 2) { - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;"); - db.execSQL("UPDATE " + TABLE_NAME + " SET multiple = 0"); - from++; - onUpdate(db, from, to); - return; - } - if (from == 3) { - // Do nothing - from++; - onUpdate(db, from, to); - return; - } - if (from == 4) { - // Do nothing -- added Category - from++; - onUpdate(db, from, to); - return; - } - if (from == 5) { - // Added width and height fields - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;"); - db.execSQL("UPDATE " + TABLE_NAME + " SET width = 0"); - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;"); - db.execSQL("UPDATE " + TABLE_NAME + " SET height = 0"); - db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;"); - db.execSQL("UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';"); - from++; - onUpdate(db, from, to); - return; - } - } + throw new RuntimeException("Unrecognized license value: " + license); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index a243330c3..db20963e7 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -93,10 +93,15 @@ class ContributionController { shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY); break; case SELECT_FROM_CAMERA: - shareIntent.setType("image/jpeg"); //FIXME: Find out appropriate mime type + //FIXME: Find out appropriate mime type + // AFAIK this is the right type for a JPEG image + // https://developer.android.com/training/sharing/send.html#send-binary-content + shareIntent.setType("image/jpeg"); shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA); break; + default: + break; } Timber.i("Image selected"); try { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java new file mode 100644 index 000000000..9d3038e03 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java @@ -0,0 +1,281 @@ +package fr.free.nrw.commons.contributions; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.RemoteException; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import java.util.Date; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; + +import fr.free.nrw.commons.settings.Prefs; + +import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS; +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.uriForId; + +public class ContributionDao { + /* + This sorts in the following order: + Currently Uploading + Failed (Sorted in ascending order of time added - FIFO) + Queued to Upload (Sorted in ascending order of time added - FIFO) + Completed (Sorted in descending order of time added) + + This is why Contribution.STATE_COMPLETED is -1. + */ + static final String CONTRIBUTION_SORT = Table.COLUMN_STATE + " DESC, " + + Table.COLUMN_UPLOADED + " DESC , (" + + Table.COLUMN_TIMESTAMP + " * " + + Table.COLUMN_STATE + ")"; + + private final Provider clientProvider; + + @Inject + public ContributionDao(@Named("contribution") Provider clientProvider) { + this.clientProvider = clientProvider; + } + + Cursor loadAllContributions() { + ContentProviderClient db = clientProvider.get(); + try { + return db.query(BASE_URI, ALL_FIELDS, "", null, CONTRIBUTION_SORT); + } catch (RemoteException e) { + return null; + } finally { + db.release(); + } + } + + public void save(Contribution contribution) { + ContentProviderClient db = clientProvider.get(); + try { + if (contribution.getContentUri() == null) { + contribution.setContentUri(db.insert(BASE_URI, toContentValues(contribution))); + } else { + db.update(contribution.getContentUri(), toContentValues(contribution), null, null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + db.release(); + } + } + + public void delete(Contribution contribution) { + ContentProviderClient db = clientProvider.get(); + try { + if (contribution.getContentUri() == null) { + // noooo + throw new RuntimeException("tried to delete item with no content URI"); + } else { + db.delete(contribution.getContentUri(), null, null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + db.release(); + } + } + + ContentValues toContentValues(Contribution contribution) { + ContentValues cv = new ContentValues(); + cv.put(Table.COLUMN_FILENAME, contribution.getFilename()); + if (contribution.getLocalUri() != null) { + cv.put(Table.COLUMN_LOCAL_URI, contribution.getLocalUri().toString()); + } + if (contribution.getImageUrl() != null) { + cv.put(Table.COLUMN_IMAGE_URL, contribution.getImageUrl()); + } + if (contribution.getDateUploaded() != null) { + cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime()); + } + cv.put(Table.COLUMN_LENGTH, contribution.getDataLength()); + cv.put(Table.COLUMN_TIMESTAMP, contribution.getTimestamp().getTime()); + cv.put(Table.COLUMN_STATE, contribution.getState()); + cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred()); + cv.put(Table.COLUMN_SOURCE, contribution.getSource()); + cv.put(Table.COLUMN_DESCRIPTION, contribution.getDescription()); + cv.put(Table.COLUMN_CREATOR, contribution.getCreator()); + cv.put(Table.COLUMN_MULTIPLE, contribution.getMultiple() ? 1 : 0); + cv.put(Table.COLUMN_WIDTH, contribution.getWidth()); + cv.put(Table.COLUMN_HEIGHT, contribution.getHeight()); + cv.put(Table.COLUMN_LICENSE, contribution.getLicense()); + return cv; + } + + public Contribution fromCursor(Cursor cursor) { + // Hardcoding column positions! + //Check that cursor has a value to avoid CursorIndexOutOfBoundsException + if (cursor.getCount() > 0) { + return new Contribution( + uriForId(cursor.getInt(0)), + cursor.getString(1), + parseUri(cursor.getString(2)), + cursor.getString(3), + parseTimestamp(cursor.getLong(4)), + cursor.getInt(5), + cursor.getLong(6), + parseTimestamp(cursor.getLong(7)), + cursor.getLong(8), + cursor.getString(9), + cursor.getString(10), + cursor.getString(11), + cursor.getInt(12) == 1, + cursor.getInt(13), + cursor.getInt(14), + cursor.getString(15)); + } + + return null; + } + + @Nullable + private static Date parseTimestamp(long timestamp) { + return timestamp == 0 ? null : new Date(timestamp); + } + + @Nullable + private static Uri parseUri(String uriString) { + return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString); + } + + public static class Table { + public static final String TABLE_NAME = "contributions"; + + public static final String COLUMN_ID = "_id"; + public static final String COLUMN_FILENAME = "filename"; + public static final String COLUMN_LOCAL_URI = "local_uri"; + public static final String COLUMN_IMAGE_URL = "image_url"; + public static final String COLUMN_TIMESTAMP = "timestamp"; + public static final String COLUMN_STATE = "state"; + public static final String COLUMN_LENGTH = "length"; + public static final String COLUMN_UPLOADED = "uploaded"; + public static final String COLUMN_TRANSFERRED = "transferred"; // Currently transferred number of bytes + public static final String COLUMN_SOURCE = "source"; + public static final String COLUMN_DESCRIPTION = "description"; + public static final String COLUMN_CREATOR = "creator"; // Initial uploader + public static final String COLUMN_MULTIPLE = "multiple"; + public static final String COLUMN_WIDTH = "width"; + public static final String COLUMN_HEIGHT = "height"; + public static final String COLUMN_LICENSE = "license"; + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + public static final String[] ALL_FIELDS = { + COLUMN_ID, + COLUMN_FILENAME, + COLUMN_LOCAL_URI, + COLUMN_IMAGE_URL, + COLUMN_TIMESTAMP, + COLUMN_STATE, + COLUMN_LENGTH, + COLUMN_UPLOADED, + COLUMN_TRANSFERRED, + COLUMN_SOURCE, + COLUMN_DESCRIPTION, + COLUMN_CREATOR, + COLUMN_MULTIPLE, + COLUMN_WIDTH, + COLUMN_HEIGHT, + COLUMN_LICENSE + }; + + public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; + + public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" + + "_id INTEGER PRIMARY KEY," + + "filename STRING," + + "local_uri STRING," + + "image_url STRING," + + "uploaded INTEGER," + + "timestamp INTEGER," + + "state INTEGER," + + "length INTEGER," + + "transferred INTEGER," + + "source STRING," + + "description STRING," + + "creator STRING," + + "multiple INTEGER," + + "width INTEGER," + + "height INTEGER," + + "LICENSE STRING" + + ");"; + + // Upgrade from version 1 -> + static final String ADD_CREATOR_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN creator STRING;"; + static final String ADD_DESCRIPTION_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN description STRING;"; + + // Upgrade from version 2 -> + static final String ADD_MULTIPLE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN multiple INTEGER;"; + static final String SET_DEFAULT_MULTIPLE = "UPDATE " + TABLE_NAME + " SET multiple = 0"; + + // Upgrade from version 5 -> + static final String ADD_WIDTH_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN width INTEGER;"; + static final String SET_DEFAULT_WIDTH = "UPDATE " + TABLE_NAME + " SET width = 0"; + static final String ADD_HEIGHT_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN height INTEGER;"; + static final String SET_DEFAULT_HEIGHT = "UPDATE " + TABLE_NAME + " SET height = 0"; + static final String ADD_LICENSE_FIELD = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN license STRING;"; + static final String SET_DEFAULT_LICENSE = "UPDATE " + TABLE_NAME + " SET license='" + Prefs.Licenses.CC_BY_SA_3 + "';"; + + + public static void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_STATEMENT); + } + + public static void onDelete(SQLiteDatabase db) { + db.execSQL(DROP_TABLE_STATEMENT); + onCreate(db); + } + + public static void onUpdate(SQLiteDatabase db, int from, int to) { + if (from == to) { + return; + } + if (from == 1) { + db.execSQL(ADD_DESCRIPTION_FIELD); + db.execSQL(ADD_CREATOR_FIELD); + from++; + onUpdate(db, from, to); + return; + } + if (from == 2) { + db.execSQL(ADD_MULTIPLE_FIELD); + db.execSQL(SET_DEFAULT_MULTIPLE); + from++; + onUpdate(db, from, to); + return; + } + if (from == 3) { + // Do nothing + from++; + onUpdate(db, from, to); + return; + } + if (from == 4) { + // Do nothing -- added Category + from++; + onUpdate(db, from, to); + return; + } + if (from == 5) { + // Added width and height fields + db.execSQL(ADD_WIDTH_FIELD); + db.execSQL(SET_DEFAULT_WIDTH); + db.execSQL(ADD_HEIGHT_FIELD); + db.execSQL(SET_DEFAULT_HEIGHT); + db.execSQL(ADD_LICENSE_FIELD); + db.execSQL(SET_DEFAULT_LICENSE); + from++; + onUpdate(db, from, to); + return; + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java index f27bfe210..5c1ecfaa0 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java @@ -9,7 +9,6 @@ import android.database.Cursor; import android.database.DataSetObserver; import android.os.Bundle; import android.os.IBinder; -import android.preference.PreferenceManager; import android.support.v4.app.FragmentManager; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; @@ -23,13 +22,17 @@ import android.widget.AdapterView; import java.util.ArrayList; +import javax.inject.Inject; +import javax.inject.Named; + import butterknife.ButterKnife; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.media.MediaDetailPagerFragment; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.upload.UploadService; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -39,15 +42,22 @@ import timber.log.Timber; import static android.content.ContentResolver.requestSync; import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; -import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS; -import static fr.free.nrw.commons.contributions.ContributionsContentProvider.AUTHORITY; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING; -public class ContributionsActivity extends AuthenticatedActivity - implements LoaderManager.LoaderCallbacks, AdapterView.OnItemClickListener, - MediaDetailPagerFragment.MediaDetailProvider, FragmentManager.OnBackStackChangedListener, - ContributionsListFragment.SourceRefresher { +public class ContributionsActivity + extends AuthenticatedActivity + implements LoaderManager.LoaderCallbacks, + AdapterView.OnItemClickListener, + MediaDetailPagerFragment.MediaDetailProvider, + FragmentManager.OnBackStackChangedListener, + ContributionsListFragment.SourceRefresher { + + @Inject MediaWikiApi mediaWikiApi; + @Inject SessionManager sessionManager; + @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject ContributionDao contributionDao; private Cursor allContributions; private ContributionsListFragment contributionsList; @@ -55,21 +65,6 @@ public class ContributionsActivity extends AuthenticatedActivity private UploadService uploadService; private boolean isUploadServiceConnected; private ArrayList observersWaitingForLoad = new ArrayList<>(); - private String CONTRIBUTION_SELECTION = ""; - - /* - This sorts in the following order: - Currently Uploading - Failed (Sorted in ascending order of time added - FIFO) - Queued to Upload (Sorted in ascending order of time added - FIFO) - Completed (Sorted in descending order of time added) - - This is why Contribution.STATE_COMPLETED is -1. - */ - private String CONTRIBUTION_SORT = Contribution.Table.COLUMN_STATE + " DESC, " - + Contribution.Table.COLUMN_UPLOADED + " DESC , (" - + Contribution.Table.COLUMN_TIMESTAMP + " * " - + Contribution.Table.COLUMN_STATE + ")"; private CompositeDisposable compositeDisposable = new CompositeDisposable(); @@ -84,7 +79,7 @@ public class ContributionsActivity extends AuthenticatedActivity @Override public void onServiceDisconnected(ComponentName componentName) { // this should never happen - throw new RuntimeException("UploadService died but the rest of the process did not!"); + Timber.e(new RuntimeException("UploadService died but the rest of the process did not!")); } }; @@ -101,12 +96,8 @@ public class ContributionsActivity extends AuthenticatedActivity @Override protected void onResume() { super.onResume(); - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean isSettingsChanged = - sharedPreferences.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false); - editor.apply(); + boolean isSettingsChanged = prefs.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false); + prefs.edit().putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false).apply(); if (isSettingsChanged) { refreshSource(); } @@ -114,16 +105,14 @@ public class ContributionsActivity extends AuthenticatedActivity @Override protected void onAuthCookieAcquired(String authCookie) { - // Do a sync every time we get here! - CommonsApplication app = ((CommonsApplication) getApplication()); - requestSync(app.getCurrentAccount(), AUTHORITY, new Bundle()); + // Do a sync everytime we get here! + requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.CONTRIBUTION_AUTHORITY, new Bundle()); Intent uploadServiceIntent = new Intent(this, UploadService.class); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); startService(uploadServiceIntent); bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); - allContributions = getContentResolver().query(BASE_URI, ALL_FIELDS, - CONTRIBUTION_SELECTION, null, CONTRIBUTION_SORT); + allContributions = contributionDao.loadAllContributions(); getSupportLoaderManager().initLoader(0, null, this); } @@ -137,17 +126,20 @@ public class ContributionsActivity extends AuthenticatedActivity // Activity can call methods in the fragment by acquiring a // reference to the Fragment from FragmentManager, using findFragmentById() FragmentManager supportFragmentManager = getSupportFragmentManager(); - contributionsList = (ContributionsListFragment) supportFragmentManager + contributionsList = (ContributionsListFragment)supportFragmentManager .findFragmentById(R.id.contributionsListFragment); supportFragmentManager.addOnBackStackChangedListener(this); if (savedInstanceState != null) { - mediaDetails = (MediaDetailPagerFragment) supportFragmentManager + mediaDetails = (MediaDetailPagerFragment)supportFragmentManager .findFragmentById(R.id.contributionsFragmentContainer); + + getSupportLoaderManager().initLoader(0, null, this); } requestAuthToken(); initDrawer(); setTitle(getString(R.string.title_activity_contributions)); + setUploadCount(); } @Override @@ -178,24 +170,23 @@ public class ContributionsActivity extends AuthenticatedActivity public void retryUpload(int i) { allContributions.moveToPosition(i); - Contribution c = Contribution.fromCursor(allContributions); + Contribution c = contributionDao.fromCursor(allContributions); if (c.getState() == STATE_FAILED) { uploadService.queue(UploadService.ACTION_UPLOAD_FILE, c); - Timber.d("Restarting for %s", c.toContentValues()); + Timber.d("Restarting for %s", c.toString()); } else { - Timber.d("Skipping re-upload for non-failed %s", c.toContentValues()); + Timber.d("Skipping re-upload for non-failed %s", c.toString()); } } public void deleteUpload(int i) { allContributions.moveToPosition(i); - Contribution c = Contribution.fromCursor(allContributions); + Contribution c = contributionDao.fromCursor(allContributions); if (c.getState() == STATE_FAILED) { - Timber.d("Deleting failed contrib %s", c.toContentValues()); - c.setContentProviderClient(getContentResolver().acquireContentProviderClient(AUTHORITY)); - c.delete(); + Timber.d("Deleting failed contrib %s", c.toString()); + contributionDao.delete(c); } else { - Timber.d("Skipping deletion for non-failed contrib %s", c.toContentValues()); + Timber.d("Skipping deletion for non-failed contrib %s", c.toString()); } } @@ -229,24 +220,23 @@ public class ContributionsActivity extends AuthenticatedActivity @Override public Loader onCreateLoader(int i, Bundle bundle) { - SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); - int uploads = sharedPref.getInt(UPLOADS_SHOWING, 100); + int uploads = prefs.getInt(UPLOADS_SHOWING, 100); return new CursorLoader(this, BASE_URI, - ALL_FIELDS, CONTRIBUTION_SELECTION, null, - CONTRIBUTION_SORT + "LIMIT " + uploads); + ALL_FIELDS, "", null, + ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads); } @Override public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + contributionsList.changeProgressBarVisibility(false); + if (contributionsList.getAdapter() == null) { contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(), - cursor, 0)); + cursor, 0, contributionDao)); } else { ((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor); } - setUploadCount(); - contributionsList.clearSyncMessage(); notifyAndMigrateDataSetObservers(); } @@ -263,7 +253,7 @@ public class ContributionsActivity extends AuthenticatedActivity // not yet ready to return data return null; } else { - return Contribution.fromCursor((Cursor) contributionsList.getAdapter().getItem(i)); + return contributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i)); } } @@ -277,19 +267,16 @@ public class ContributionsActivity extends AuthenticatedActivity @SuppressWarnings("ConstantConditions") private void setUploadCount() { - CommonsApplication app = ((CommonsApplication) getApplication()); - compositeDisposable.add( - app.getMWApi() - .getUploadCount(app.getCurrentAccount().name) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - uploadCount -> getSupportActionBar().setSubtitle(getResources() - .getQuantityString(R.plurals.contributions_subtitle, - uploadCount, uploadCount)), - t -> Timber.e(t, "Fetching upload count failed") - ) - ); + compositeDisposable.add(mediaWikiApi + .getUploadCount(sessionManager.getCurrentAccount().name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + uploadCount -> getSupportActionBar().setSubtitle(getResources() + .getQuantityString(R.plurals.contributions_subtitle, + uploadCount, uploadCount)), + t -> Timber.e(t, "Fetching upload count failed") + )); } @Override @@ -341,9 +328,4 @@ public class ContributionsActivity extends AuthenticatedActivity public void refreshSource() { getSupportLoaderManager().restartLoader(0, null, this); } - - public static void startYourself(Context context) { - context.startActivity(new Intent(context, ContributionsActivity.class)); - } - } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java index aae9fa3bc..0a68ac626 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons.contributions; -import android.content.ContentProvider; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; @@ -10,36 +9,36 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.text.TextUtils; -import fr.free.nrw.commons.CommonsApplication; +import javax.inject.Inject; + +import fr.free.nrw.commons.data.DBOpenHelper; +import fr.free.nrw.commons.di.CommonsDaggerContentProvider; import timber.log.Timber; import static android.content.UriMatcher.NO_MATCH; -import static fr.free.nrw.commons.contributions.Contribution.Table.ALL_FIELDS; -import static fr.free.nrw.commons.contributions.Contribution.Table.TABLE_NAME; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.TABLE_NAME; -public class ContributionsContentProvider extends ContentProvider { +public class ContributionsContentProvider extends CommonsDaggerContentProvider { private static final int CONTRIBUTIONS = 1; private static final int CONTRIBUTIONS_ID = 2; private static final String BASE_PATH = "contributions"; private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH); - public static final String AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider"; + public static final String CONTRIBUTION_AUTHORITY = "fr.free.nrw.commons.contributions.contentprovider"; - public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH); + public static final Uri BASE_URI = Uri.parse("content://" + CONTRIBUTION_AUTHORITY + "/" + BASE_PATH); static { - uriMatcher.addURI(AUTHORITY, BASE_PATH, CONTRIBUTIONS); - uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID); + uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH, CONTRIBUTIONS); + uriMatcher.addURI(CONTRIBUTION_AUTHORITY, BASE_PATH + "/#", CONTRIBUTIONS_ID); } public static Uri uriForId(int id) { return Uri.parse(BASE_URI.toString() + "/" + id); } - @Override - public boolean onCreate() { - return false; - } + @Inject DBOpenHelper dbOpenHelper; @SuppressWarnings("ConstantConditions") @Override @@ -50,8 +49,7 @@ public class ContributionsContentProvider extends ContentProvider { int uriType = uriMatcher.match(uri); - CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); - SQLiteDatabase db = app.getDBOpenHelper().getReadableDatabase(); + SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); Cursor cursor; switch (uriType) { @@ -87,8 +85,7 @@ public class ContributionsContentProvider extends ContentProvider { @Override public Uri insert(@NonNull Uri uri, ContentValues contentValues) { int uriType = uriMatcher.match(uri); - CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); - SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase(); + SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); long id; switch (uriType) { case CONTRIBUTIONS: @@ -107,13 +104,12 @@ public class ContributionsContentProvider extends ContentProvider { int rows; int uriType = uriMatcher.match(uri); - CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); - SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase(); + SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); switch (uriType) { case CONTRIBUTIONS_ID: Timber.d("Deleting contribution id %s", uri.getLastPathSegment()); - rows = sqlDB.delete(TABLE_NAME, + rows = db.delete(TABLE_NAME, "_id = ?", new String[]{uri.getLastPathSegment()} ); @@ -130,8 +126,7 @@ public class ContributionsContentProvider extends ContentProvider { public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { Timber.d("Hello, bulk insert! (ContributionsContentProvider)"); int uriType = uriMatcher.match(uri); - CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); - SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase(); + SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); sqlDB.beginTransaction(); switch (uriType) { case CONTRIBUTIONS: @@ -162,8 +157,7 @@ public class ContributionsContentProvider extends ContentProvider { error out otherwise. */ int uriType = uriMatcher.match(uri); - CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); - SQLiteDatabase sqlDB = app.getDBOpenHelper().getWritableDatabase(); + SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); int rowsUpdated; switch (uriType) { case CONTRIBUTIONS: @@ -175,7 +169,7 @@ public class ContributionsContentProvider extends ContentProvider { if (TextUtils.isEmpty(selection)) { rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, - Contribution.Table.COLUMN_ID + " = ?", + ContributionDao.Table.COLUMN_ID + " = ?", new String[]{String.valueOf(id)}); } else { throw new IllegalArgumentException( diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java index 0882d09c4..a31caf54f 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.java @@ -11,8 +11,11 @@ import fr.free.nrw.commons.R; class ContributionsListAdapter extends CursorAdapter { - public ContributionsListAdapter(Context context, Cursor c, int flags) { + private final ContributionDao contributionDao; + + public ContributionsListAdapter(Context context, Cursor c, int flags, ContributionDao contributionDao) { super(context, c, flags); + this.contributionDao = contributionDao; } @Override @@ -26,7 +29,7 @@ class ContributionsListAdapter extends CursorAdapter { @Override public void bindView(View view, Context context, Cursor cursor) { final ContributionViewHolder views = (ContributionViewHolder)view.getTag(); - final Contribution contribution = Contribution.fromCursor(cursor); + final Contribution contribution = contributionDao.fromCursor(cursor); views.imageView.setMedia(contribution); views.titleView.setText(contribution.getDisplayTitle()); @@ -34,7 +37,7 @@ class ContributionsListAdapter extends CursorAdapter { views.seqNumView.setText(String.valueOf(cursor.getPosition() + 1)); views.seqNumView.setVisibility(View.VISIBLE); - switch(contribution.getState()) { + switch (contribution.getState()) { case Contribution.STATE_COMPLETED: views.stateView.setVisibility(View.GONE); views.progressView.setVisibility(View.GONE); @@ -50,7 +53,7 @@ class ContributionsListAdapter extends CursorAdapter { views.progressView.setVisibility(View.VISIBLE); long total = contribution.getDataLength(); long transferred = contribution.getTransferred(); - if(transferred == 0 || transferred >= total) { + if (transferred == 0 || transferred >= total) { views.progressView.setIndeterminate(true); } else { views.progressView.setProgress((int)(((double)transferred / (double)total) * 100)); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index 83dbced97..25bf6eb93 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -5,9 +5,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; -import android.preference.PreferenceManager; import android.support.annotation.NonNull; -import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.view.LayoutInflater; @@ -19,30 +17,43 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.GridView; import android.widget.ListAdapter; +import android.widget.ProgressBar; import android.widget.TextView; +import java.util.Arrays; + +import javax.inject.Inject; +import javax.inject.Named; + import butterknife.BindView; import butterknife.ButterKnife; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.nearby.NearbyActivity; import timber.log.Timber; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; import static android.app.Activity.RESULT_OK; -import static android.content.Context.MODE_PRIVATE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.view.View.GONE; -public class ContributionsListFragment extends Fragment { +public class ContributionsListFragment extends CommonsDaggerSupportFragment { @BindView(R.id.contributionsList) GridView contributionsList; @BindView(R.id.waitingMessage) TextView waitingMessage; - @BindView(R.id.emptyMessage) - TextView emptyMessage; + @BindView(R.id.loadingContributionsProgressBar) + ProgressBar progressBar; + + @Inject + @Named("prefs") + SharedPreferences prefs; + @Inject + @Named("default_preferences") + SharedPreferences defaultPrefs; + private ContributionController controller; @Override @@ -57,7 +68,6 @@ public class ContributionsListFragment extends Fragment { } //TODO: Should this be in onResume? - SharedPreferences prefs = getActivity().getSharedPreferences("prefs", MODE_PRIVATE); String lastModified = prefs.getString("lastSyncTimestamp", ""); Timber.d("Last Sync Timestamp: %s", lastModified); @@ -67,6 +77,7 @@ public class ContributionsListFragment extends Fragment { waitingMessage.setVisibility(GONE); } + changeProgressBarVisibility(true); return v; } @@ -78,6 +89,10 @@ public class ContributionsListFragment extends Fragment { this.contributionsList.setAdapter(adapter); } + public void changeProgressBarVisibility(boolean isVisible) { + this.progressBar.setVisibility(isVisible ? View.VISIBLE : View.GONE); + } + @Override public void onSaveInstanceState(Bundle outState) { if (outState == null) { @@ -155,9 +170,7 @@ public class ContributionsListFragment extends Fragment { return true; case R.id.menu_from_camera: - SharedPreferences sharedPref = PreferenceManager - .getDefaultSharedPreferences(CommonsApplication.getInstance()); - boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true); + boolean useExtStorage = defaultPrefs.getBoolean("useExternalStorage", true); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && useExtStorage) { // Here, thisActivity is the current activity if (ContextCompat.checkSelfPermission(getActivity(), WRITE_EXTERNAL_STORAGE) @@ -201,7 +214,7 @@ public class ContributionsListFragment extends Fragment { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Timber.d("onRequestPermissionsResult: req code = " + " perm = " - + permissions + " grant =" + grantResults); + + Arrays.toString(permissions) + " grant =" + Arrays.toString(grantResults)); switch (requestCode) { // 1 = Storage allowed when gallery selected @@ -235,12 +248,17 @@ public class ContributionsListFragment extends Fragment { menu.clear(); // See http://stackoverflow.com/a/8495697/17865 inflater.inflate(R.menu.fragment_contributions_list, menu); - CommonsApplication app = (CommonsApplication) getContext().getApplicationContext(); - if (!app.deviceHasCamera()) { + if (!deviceHasCamera()) { menu.findItem(R.id.menu_from_camera).setEnabled(false); } } + public boolean deviceHasCamera() { + PackageManager pm = getContext().getPackageManager(); + return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) || + pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java index fdcc52380..aedc3f789 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java @@ -13,21 +13,27 @@ import android.os.RemoteException; import android.text.TextUtils; import java.io.IOException; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +import javax.inject.Inject; +import javax.inject.Named; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.mwapi.LogEventResult; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; -import static android.content.Context.MODE_PRIVATE; import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED; -import static fr.free.nrw.commons.contributions.Contribution.Table.COLUMN_FILENAME; +import static fr.free.nrw.commons.contributions.ContributionDao.Table.COLUMN_FILENAME; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; +@SuppressWarnings("WeakerAccess") public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { private static final String[] existsQuery = {COLUMN_FILENAME}; @@ -35,6 +41,10 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { private static final ContentValues[] EMPTY = {}; private static int COMMIT_THRESHOLD = 10; + @SuppressWarnings("WeakerAccess") + @Inject MediaWikiApi mwApi; + @Inject @Named("prefs") SharedPreferences prefs; + public ContributionsSyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); } @@ -47,6 +57,9 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { } private boolean fileExists(ContentProviderClient client, String filename) { + if (filename == null) { + return false; + } Cursor cursor = null; try { cursor = client.query(BASE_URI, @@ -68,19 +81,23 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { @Override public void onPerformSync(Account account, Bundle bundle, String authority, ContentProviderClient contentProviderClient, SyncResult syncResult) { + ApplicationlessInjection + .getInstance(getContext() + .getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); // This code is fraught with possibilities of race conditions, but lalalalala I can't hear you! String user = account.name; - MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); - SharedPreferences prefs = getContext().getSharedPreferences("prefs", MODE_PRIVATE); String lastModified = prefs.getString("lastSyncTimestamp", ""); Date curTime = new Date(); LogEventResult result; Boolean done = false; String queryContinue = null; + ContributionDao contributionDao = new ContributionDao(() -> contentProviderClient); while (!done) { try { - result = api.logEvents(user, lastModified, queryContinue, getLimit()); + result = mwApi.logEvents(user, lastModified, queryContinue, getLimit()); } catch (IOException e) { // There isn't really much we can do, eh? // FIXME: Perhaps add EventLogging? @@ -109,7 +126,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { "", -1, dateUpdated, dateUpdated, user, "", ""); contrib.setState(STATE_COMPLETED); - imageValues.add(contrib.toContentValues()); + imageValues.add(contributionDao.toContentValues(contrib)); if (imageValues.size() % COMMIT_THRESHOLD == 0) { try { @@ -134,8 +151,13 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { done = true; } } - prefs.edit().putString("lastSyncTimestamp", Utils.toMWDate(curTime)).apply(); + prefs.edit().putString("lastSyncTimestamp", toMWDate(curTime)).apply(); Timber.d("Oh hai, everyone! Look, a kitty!"); } + private String toMWDate(Date date) { + SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC + isoFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return isoFormat.format(date); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java index 8ef9f336c..35305c5ba 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java @@ -4,16 +4,18 @@ import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.modifications.ModifierSequence; +import fr.free.nrw.commons.category.CategoryDao; +import fr.free.nrw.commons.contributions.ContributionDao; +import fr.free.nrw.commons.modifications.ModifierSequenceDao; -public class DBOpenHelper extends SQLiteOpenHelper{ +public class DBOpenHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "commons.db"; private static final int DATABASE_VERSION = 6; /** - * Do not use, please call CommonsApplication.getDBOpenHelper() + * Do not use directly - @Inject an instance where it's needed and let + * dependency injection take care of managing this as a singleton. */ public DBOpenHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -21,15 +23,15 @@ public class DBOpenHelper extends SQLiteOpenHelper{ @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { - Contribution.Table.onCreate(sqLiteDatabase); - ModifierSequence.Table.onCreate(sqLiteDatabase); - Category.Table.onCreate(sqLiteDatabase); + ContributionDao.Table.onCreate(sqLiteDatabase); + ModifierSequenceDao.Table.onCreate(sqLiteDatabase); + CategoryDao.Table.onCreate(sqLiteDatabase); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) { - Contribution.Table.onUpdate(sqLiteDatabase, from, to); - ModifierSequence.Table.onUpdate(sqLiteDatabase, from, to); - Category.Table.onUpdate(sqLiteDatabase, from, to); + ContributionDao.Table.onUpdate(sqLiteDatabase, from, to); + ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to); + CategoryDao.Table.onUpdate(sqLiteDatabase, from, to); } } diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java new file mode 100644 index 000000000..e4fb13427 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -0,0 +1,49 @@ +package fr.free.nrw.commons.di; + +import dagger.Module; +import dagger.android.ContributesAndroidInjector; +import fr.free.nrw.commons.AboutActivity; +import fr.free.nrw.commons.WelcomeActivity; +import fr.free.nrw.commons.auth.LoginActivity; +import fr.free.nrw.commons.auth.SignupActivity; +import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.nearby.NearbyActivity; +import fr.free.nrw.commons.notification.NotificationActivity; +import fr.free.nrw.commons.settings.SettingsActivity; +import fr.free.nrw.commons.upload.MultipleShareActivity; +import fr.free.nrw.commons.upload.ShareActivity; + +@Module +@SuppressWarnings({"WeakerAccess", "unused"}) +public abstract class ActivityBuilderModule { + + @ContributesAndroidInjector + abstract LoginActivity bindLoginActivity(); + + @ContributesAndroidInjector + abstract WelcomeActivity bindWelcomeActivity(); + + @ContributesAndroidInjector + abstract ShareActivity bindShareActivity(); + + @ContributesAndroidInjector + abstract MultipleShareActivity bindMultipleShareActivity(); + + @ContributesAndroidInjector + abstract ContributionsActivity bindContributionsActivity(); + + @ContributesAndroidInjector + abstract SettingsActivity bindSettingsActivity(); + + @ContributesAndroidInjector + abstract AboutActivity bindAboutActivity(); + + @ContributesAndroidInjector + abstract SignupActivity bindSignupActivity(); + + @ContributesAndroidInjector + abstract NearbyActivity bindNearbyActivity(); + + @ContributesAndroidInjector + abstract NotificationActivity bindNotificationActivity(); +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java b/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java new file mode 100644 index 000000000..2faf09b83 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java @@ -0,0 +1,93 @@ +package fr.free.nrw.commons.di; + +import android.app.Activity; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ContentProvider; +import android.content.Context; +import android.support.v4.app.Fragment; + +import javax.inject.Inject; + +import dagger.android.AndroidInjector; +import dagger.android.DispatchingAndroidInjector; +import dagger.android.HasActivityInjector; +import dagger.android.HasBroadcastReceiverInjector; +import dagger.android.HasContentProviderInjector; +import dagger.android.HasFragmentInjector; +import dagger.android.HasServiceInjector; +import dagger.android.support.HasSupportFragmentInjector; + +public class ApplicationlessInjection + implements + HasActivityInjector, + HasFragmentInjector, + HasSupportFragmentInjector, + HasServiceInjector, + HasBroadcastReceiverInjector, + HasContentProviderInjector { + + private static ApplicationlessInjection instance = null; + + @Inject DispatchingAndroidInjector activityInjector; + @Inject DispatchingAndroidInjector broadcastReceiverInjector; + @Inject DispatchingAndroidInjector fragmentInjector; + @Inject DispatchingAndroidInjector supportFragmentInjector; + @Inject DispatchingAndroidInjector serviceInjector; + @Inject DispatchingAndroidInjector contentProviderInjector; + + private CommonsApplicationComponent commonsApplicationComponent; + + public ApplicationlessInjection(Context applicationContext) { + commonsApplicationComponent = DaggerCommonsApplicationComponent.builder() + .appModule(new CommonsApplicationModule(applicationContext)).build(); + commonsApplicationComponent.inject(this); + } + + @Override + public DispatchingAndroidInjector activityInjector() { + return activityInjector; + } + + @Override + public DispatchingAndroidInjector fragmentInjector() { + return fragmentInjector; + } + + @Override + public DispatchingAndroidInjector supportFragmentInjector() { + return supportFragmentInjector; + } + + @Override + public DispatchingAndroidInjector broadcastReceiverInjector() { + return broadcastReceiverInjector; + } + + @Override + public DispatchingAndroidInjector serviceInjector() { + return serviceInjector; + } + + @Override + public AndroidInjector contentProviderInjector() { + return contentProviderInjector; + } + + public CommonsApplicationComponent getCommonsApplicationComponent() { + return commonsApplicationComponent; + } + + public static ApplicationlessInjection getInstance(Context applicationContext) { + if (instance == null) { + synchronized (ApplicationlessInjection.class) { + if (instance == null) { + instance = new ApplicationlessInjection(applicationContext); + } + } + } + + return instance; + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java new file mode 100644 index 000000000..f5974d519 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -0,0 +1,49 @@ +package fr.free.nrw.commons.di; + +import javax.inject.Singleton; + +import dagger.Component; +import dagger.android.AndroidInjectionModule; +import dagger.android.AndroidInjector; +import dagger.android.support.AndroidSupportInjectionModule; +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.MediaWikiImageView; +import fr.free.nrw.commons.auth.LoginActivity; +import fr.free.nrw.commons.contributions.ContributionsSyncAdapter; +import fr.free.nrw.commons.modifications.ModificationsSyncAdapter; +import fr.free.nrw.commons.settings.SettingsFragment; + +@Singleton +@Component(modules = { + CommonsApplicationModule.class, + AndroidInjectionModule.class, + AndroidSupportInjectionModule.class, + ActivityBuilderModule.class, + FragmentBuilderModule.class, + ServiceBuilderModule.class, + ContentProviderBuilderModule.class +}) +public interface CommonsApplicationComponent extends AndroidInjector { + void inject(CommonsApplication application); + + void inject(ContributionsSyncAdapter syncAdapter); + + void inject(ModificationsSyncAdapter syncAdapter); + + void inject(MediaWikiImageView mediaWikiImageView); + + void inject(LoginActivity activity); + + void inject(SettingsFragment fragment); + + @Override + void inject(ApplicationlessInjection instance); + + @Component.Builder + @SuppressWarnings({"WeakerAccess", "unused"}) + interface Builder { + Builder appModule(CommonsApplicationModule applicationModule); + + CommonsApplicationComponent build(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java new file mode 100644 index 000000000..efc9c29b6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -0,0 +1,139 @@ +package fr.free.nrw.commons.di; + +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.v4.util.LruCache; + +import javax.inject.Named; +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.auth.AccountUtil; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.caching.CacheController; +import fr.free.nrw.commons.data.DBOpenHelper; +import fr.free.nrw.commons.location.LocationServiceManager; +import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.nearby.NearbyPlaces; +import fr.free.nrw.commons.upload.UploadController; + +import static android.content.Context.MODE_PRIVATE; +import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY; +import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MODIFICATIONS_AUTHORITY; + +@Module +@SuppressWarnings({"WeakerAccess", "unused"}) +public class CommonsApplicationModule { + public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider"; + public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; + + private CommonsApplication application; + private Context applicationContext; + + public CommonsApplicationModule(Context applicationContext) { + this.applicationContext = applicationContext; + } + + @Provides + public Context providesApplicationContext() { + return this.applicationContext; + } + + @Provides + public AccountUtil providesAccountUtil(Context context) { + return new AccountUtil(context); + } + + @Provides + @Named("category") + public ContentProviderClient provideCategoryContentProviderClient(Context context) { + return context.getContentResolver().acquireContentProviderClient(CATEGORY_AUTHORITY); + } + + @Provides + @Named("contribution") + public ContentProviderClient provideContributionContentProviderClient(Context context) { + return context.getContentResolver().acquireContentProviderClient(CONTRIBUTION_AUTHORITY); + } + + @Provides + @Named("modification") + public ContentProviderClient provideModificationContentProviderClient(Context context) { + return context.getContentResolver().acquireContentProviderClient(MODIFICATIONS_AUTHORITY); + } + + @Provides + @Named("application_preferences") + public SharedPreferences providesApplicationSharedPreferences(Context context) { + return context.getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE); + } + + @Provides + @Named("default_preferences") + public SharedPreferences providesDefaultSharedPreferences(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context); + } + + @Provides + @Named("prefs") + public SharedPreferences providesOtherSharedPreferences(Context context) { + return context.getSharedPreferences("prefs", MODE_PRIVATE); + } + + @Provides + public UploadController providesUploadController(Context context, + SessionManager sessionManager, + @Named("default_preferences") SharedPreferences sharedPreferences) { + return new UploadController(sessionManager, context, sharedPreferences); + } + + @Provides + @Singleton + public SessionManager providesSessionManager(Context context, + MediaWikiApi mediaWikiApi, + @Named("default_preferences") SharedPreferences sharedPreferences) { + return new SessionManager(context, mediaWikiApi, sharedPreferences); + } + + @Provides + @Singleton + public MediaWikiApi provideMediaWikiApi(Context context, @Named("default_preferences") SharedPreferences sharedPreferences) { + return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, sharedPreferences); + } + + @Provides + @Singleton + public LocationServiceManager provideLocationServiceManager(Context context) { + return new LocationServiceManager(context); + } + + @Provides + @Singleton + public CacheController provideCacheController() { + return new CacheController(); + } + + @Provides + @Singleton + public DBOpenHelper provideDBOpenHelper(Context context) { + return new DBOpenHelper(context); + } + + @Provides + @Singleton + public NearbyPlaces provideNearbyPlaces() { + return new NearbyPlaces(); + } + + @Provides + @Singleton + public LruCache provideLruCache() { + return new LruCache<>(1024); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java new file mode 100644 index 000000000..9f06725de --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java @@ -0,0 +1,43 @@ +package fr.free.nrw.commons.di; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; + +import javax.inject.Inject; + +import dagger.android.AndroidInjector; +import dagger.android.DispatchingAndroidInjector; +import dagger.android.support.HasSupportFragmentInjector; + +public abstract class CommonsDaggerAppCompatActivity extends AppCompatActivity implements HasSupportFragmentInjector { + + @Inject + DispatchingAndroidInjector supportFragmentInjector; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + inject(); + super.onCreate(savedInstanceState); + } + + @Override + public AndroidInjector supportFragmentInjector() { + return supportFragmentInjector; + } + + private void inject() { + ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext()); + + AndroidInjector activityInjector = injection.activityInjector(); + + if (activityInjector == null) { + throw new NullPointerException("ApplicationlessInjection.activityInjector() returned null"); + } + + activityInjector.inject(this); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java new file mode 100644 index 000000000..3c4cb9914 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java @@ -0,0 +1,31 @@ +package fr.free.nrw.commons.di; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import dagger.android.AndroidInjector; + +public abstract class CommonsDaggerBroadcastReceiver extends BroadcastReceiver { + + public CommonsDaggerBroadcastReceiver() { + super(); + } + + @Override + public void onReceive(Context context, Intent intent) { + inject(context); + } + + private void inject(Context context) { + ApplicationlessInjection injection = ApplicationlessInjection.getInstance(context.getApplicationContext()); + + AndroidInjector serviceInjector = injection.broadcastReceiverInjector(); + + if (serviceInjector == null) { + throw new NullPointerException("ApplicationlessInjection.broadcastReceiverInjector() returned null"); + } + serviceInjector.inject(this); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java new file mode 100644 index 000000000..38506c4ca --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java @@ -0,0 +1,32 @@ +package fr.free.nrw.commons.di; + +import android.content.ContentProvider; + +import dagger.android.AndroidInjector; + + +public abstract class CommonsDaggerContentProvider extends ContentProvider { + + public CommonsDaggerContentProvider() { + super(); + } + + @Override + public boolean onCreate() { + inject(); + return true; + } + + private void inject() { + ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getContext()); + + AndroidInjector serviceInjector = injection.contentProviderInjector(); + + if (serviceInjector == null) { + throw new NullPointerException("ApplicationlessInjection.contentProviderInjector() returned null"); + } + + serviceInjector.inject(this); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java new file mode 100644 index 000000000..995c517a1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java @@ -0,0 +1,32 @@ +package fr.free.nrw.commons.di; + +import android.app.IntentService; +import android.app.Service; + +import dagger.android.AndroidInjector; + +public abstract class CommonsDaggerIntentService extends IntentService { + + public CommonsDaggerIntentService(String name) { + super(name); + } + + @Override + public void onCreate() { + inject(); + super.onCreate(); + } + + private void inject() { + ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext()); + + AndroidInjector serviceInjector = injection.serviceInjector(); + + if (serviceInjector == null) { + throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null"); + } + + serviceInjector.inject(this); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java new file mode 100644 index 000000000..dc6c10b9f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java @@ -0,0 +1,31 @@ +package fr.free.nrw.commons.di; + +import android.app.Service; + +import dagger.android.AndroidInjector; + +public abstract class CommonsDaggerService extends Service { + + public CommonsDaggerService() { + super(); + } + + @Override + public void onCreate() { + inject(); + super.onCreate(); + } + + private void inject() { + ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext()); + + AndroidInjector serviceInjector = injection.serviceInjector(); + + if (serviceInjector == null) { + throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null"); + } + + serviceInjector.inject(this); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java new file mode 100644 index 000000000..8c33e7a98 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java @@ -0,0 +1,65 @@ +package fr.free.nrw.commons.di; + +import android.app.Activity; +import android.content.Context; +import android.support.v4.app.Fragment; + +import javax.inject.Inject; + +import dagger.android.AndroidInjector; +import dagger.android.DispatchingAndroidInjector; +import dagger.android.support.HasSupportFragmentInjector; + +public abstract class CommonsDaggerSupportFragment extends Fragment implements HasSupportFragmentInjector { + + @Inject + DispatchingAndroidInjector childFragmentInjector; + + @Override + public void onAttach(Context context) { + inject(); + super.onAttach(context); + } + + @Override + public AndroidInjector supportFragmentInjector() { + return childFragmentInjector; + } + + + public void inject() { + HasSupportFragmentInjector hasSupportFragmentInjector = findHasFragmentInjector(); + + AndroidInjector fragmentInjector = hasSupportFragmentInjector.supportFragmentInjector(); + + if (fragmentInjector == null) { + throw new NullPointerException(String.format("%s.supportFragmentInjector() returned null", hasSupportFragmentInjector.getClass().getCanonicalName())); + } + + fragmentInjector.inject(this); + } + + private HasSupportFragmentInjector findHasFragmentInjector() { + Fragment parentFragment = this; + + while ((parentFragment = parentFragment.getParentFragment()) != null) { + if (parentFragment instanceof HasSupportFragmentInjector) { + return (HasSupportFragmentInjector) parentFragment; + } + } + + Activity activity = getActivity(); + + if (activity instanceof HasSupportFragmentInjector) { + return (HasSupportFragmentInjector) activity; + } + + ApplicationlessInjection injection = ApplicationlessInjection.getInstance(activity.getApplicationContext()); + if (injection != null) { + return injection; + } + + throw new IllegalArgumentException(String.format("No injector was found for %s", getClass().getCanonicalName())); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java new file mode 100644 index 000000000..f18c331c5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java @@ -0,0 +1,22 @@ +package fr.free.nrw.commons.di; + +import dagger.Module; +import dagger.android.ContributesAndroidInjector; +import fr.free.nrw.commons.category.CategoryContentProvider; +import fr.free.nrw.commons.contributions.ContributionsContentProvider; +import fr.free.nrw.commons.modifications.ModificationsContentProvider; + +@Module +@SuppressWarnings({"WeakerAccess", "unused"}) +public abstract class ContentProviderBuilderModule { + + @ContributesAndroidInjector + abstract ContributionsContentProvider bindContributionsContentProvider(); + + @ContributesAndroidInjector + abstract ModificationsContentProvider bindModificationsContentProvider(); + + @ContributesAndroidInjector + abstract CategoryContentProvider bindCategoryContentProvider(); + +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java new file mode 100644 index 000000000..ca7340cb1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -0,0 +1,46 @@ +package fr.free.nrw.commons.di; + +import dagger.Module; +import dagger.android.ContributesAndroidInjector; +import fr.free.nrw.commons.category.CategorizationFragment; +import fr.free.nrw.commons.contributions.ContributionsListFragment; +import fr.free.nrw.commons.media.MediaDetailFragment; +import fr.free.nrw.commons.media.MediaDetailPagerFragment; +import fr.free.nrw.commons.nearby.NearbyListFragment; +import fr.free.nrw.commons.nearby.NoPermissionsFragment; +import fr.free.nrw.commons.settings.SettingsFragment; +import fr.free.nrw.commons.upload.MultipleUploadListFragment; +import fr.free.nrw.commons.upload.SingleUploadFragment; + +@Module +@SuppressWarnings({"WeakerAccess", "unused"}) +public abstract class FragmentBuilderModule { + + @ContributesAndroidInjector + abstract CategorizationFragment bindCategorizationFragment(); + + @ContributesAndroidInjector + abstract ContributionsListFragment bindContributionsListFragment(); + + @ContributesAndroidInjector + abstract MediaDetailFragment bindMediaDetailFragment(); + + @ContributesAndroidInjector + abstract MediaDetailPagerFragment bindMediaDetailPagerFragment(); + + @ContributesAndroidInjector + abstract NearbyListFragment bindNearbyListFragment(); + + @ContributesAndroidInjector + abstract NoPermissionsFragment bindNoPermissionsFragment(); + + @ContributesAndroidInjector + abstract SettingsFragment bindSettingsFragment(); + + @ContributesAndroidInjector + abstract MultipleUploadListFragment bindMultipleUploadListFragment(); + + @ContributesAndroidInjector + abstract SingleUploadFragment bindSingleUploadFragment(); + +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java new file mode 100644 index 000000000..2d4072d15 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java @@ -0,0 +1,18 @@ +package fr.free.nrw.commons.di; + +import dagger.Module; +import dagger.android.ContributesAndroidInjector; +import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService; +import fr.free.nrw.commons.upload.UploadService; + +@Module +@SuppressWarnings({"WeakerAccess", "unused"}) +public abstract class ServiceBuilderModule { + + @ContributesAndroidInjector + abstract UploadService bindUploadService(); + + @ContributesAndroidInjector + abstract WikiAccountAuthenticatorService bindWikiAccountAuthenticatorService(); + +} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LatLng.java b/app/src/main/java/fr/free/nrw/commons/location/LatLng.java index 1e3202914..494cce077 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LatLng.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LatLng.java @@ -1,125 +1,156 @@ -package fr.free.nrw.commons.location; - -public class LatLng { - - private final double latitude; - private final double longitude; - private final float accuracy; - - /** Accepts latitude and longitude. - * North and South values are cut off at 90° - * - * @param latitude double value - * @param longitude double value - */ - public LatLng(double latitude, double longitude, float accuracy) { - if(-180.0D <= longitude && longitude < 180.0D) { - this.longitude = longitude; - } else { - this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D; - } - this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude)); - this.accuracy = accuracy; - } - - public int hashCode() { - boolean var1 = true; - byte var2 = 1; - long var3 = Double.doubleToLongBits(this.latitude); - int var5 = 31 * var2 + (int)(var3 ^ var3 >>> 32); - var3 = Double.doubleToLongBits(this.longitude); - var5 = 31 * var5 + (int)(var3 ^ var3 >>> 32); - return var5; - } - - public boolean equals(Object o) { - if(this == o) { - return true; - } else if(!(o instanceof LatLng)) { - return false; - } else { - LatLng var2 = (LatLng)o; - return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude); - } - } - - public String toString() { - return "lat/lng: (" + this.latitude + "," + this.longitude + ")"; - } - - /** - * Rounds the float to 4 digits and returns absolute value. - * - * @param coordinate A coordinate value as string. - * @return String of the rounded number. - */ - private String formatCoordinate(double coordinate) { - double roundedNumber = Math.round(coordinate * 10000d) / 10000d; - double absoluteNumber = Math.abs(roundedNumber); - return String.valueOf(absoluteNumber); - } - - /** - * Returns "N" or "S" depending on the latitude. - * - * @return "N" or "S". - */ - private String getNorthSouth() { - if (this.latitude < 0) { - return "S"; - } - - return "N"; - } - - /** - * Returns "E" or "W" depending on the longitude. - * - * @return "E" or "W". - */ - private String getEastWest() { - if (this.longitude >= 0 && this.longitude < 180) { - return "E"; - } - - return "W"; - } - - /** - * Returns a nicely formatted coordinate string. Used e.g. in - * the detail view. - * - * @return The formatted string. - */ - public String getPrettyCoordinateString() { - return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", " - + formatCoordinate(this.longitude) + " " + this.getEastWest(); - } - - /** - * Return the location accuracy in meter. - * - * @return float - */ - public float getAccuracy() { - return accuracy; - } - - /** - * Return the longitude in degrees. - * - * @return double - */ - public double getLongitude() { - return longitude; - } - - /** - * Return the latitude in degrees. - * - * @return double - */ - public double getLatitude() { - return latitude; - } -} +package fr.free.nrw.commons.location; + +import android.location.Location; +import android.support.annotation.NonNull; + +/** + * a latitude and longitude point with accuracy information, often of a picture + */ +public class LatLng { + + private final double latitude; + private final double longitude; + private final float accuracy; + + /** + * Accepts latitude and longitude. + * North and South values are cut off at 90° + * + * @param latitude the latitude + * @param longitude the longitude + * @param accuracy the accuracy + * + * Examples: + * the Statue of Liberty is located at 40.69° N, 74.04° W + * The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0) + * where positive signifies north, east and negative signifies south, west. + */ + public LatLng(double latitude, double longitude, float accuracy) { + if (-180.0D <= longitude && longitude < 180.0D) { + this.longitude = longitude; + } else { + this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D; + } + this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude)); + this.accuracy = accuracy; + } + + /** + * gets the latitude and longitude of a given non-null location + * @param location the non-null location of the user + * @return LatLng the Latitude and Longitude of a given location + */ + public static LatLng from(@NonNull Location location) { + return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy()); + } + + /** + * creates a hash code for the longitude and longitude + */ + public int hashCode() { + byte var1 = 1; + long var2 = Double.doubleToLongBits(this.latitude); + int var3 = 31 * var1 + (int)(var2 ^ var2 >>> 32); + var2 = Double.doubleToLongBits(this.longitude); + var3 = 31 * var3 + (int)(var2 ^ var2 >>> 32); + return var3; + } + + /** + * checks for equality of two LatLng objects + * @param o the second LatLng object + */ + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (!(o instanceof LatLng)) { + return false; + } else { + LatLng var2 = (LatLng)o; + return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude); + } + } + + /** + * returns a string representation of the latitude and longitude + */ + public String toString() { + return "lat/lng: (" + this.latitude + "," + this.longitude + ")"; + } + + /** + * Rounds the float to 4 digits and returns absolute value. + * + * @param coordinate A coordinate value as string. + * @return String of the rounded number. + */ + private String formatCoordinate(double coordinate) { + double roundedNumber = Math.round(coordinate * 10000d) / 10000d; + double absoluteNumber = Math.abs(roundedNumber); + return String.valueOf(absoluteNumber); + } + + /** + * Returns "N" or "S" depending on the latitude. + * + * @return "N" or "S". + */ + private String getNorthSouth() { + if (this.latitude < 0) { + return "S"; + } + + return "N"; + } + + /** + * Returns "E" or "W" depending on the longitude. + * + * @return "E" or "W". + */ + private String getEastWest() { + if (this.longitude >= 0 && this.longitude < 180) { + return "E"; + } + + return "W"; + } + + /** + * Returns a nicely formatted coordinate string. Used e.g. in + * the detail view. + * + * @return The formatted string. + */ + public String getPrettyCoordinateString() { + return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", " + + formatCoordinate(this.longitude) + " " + this.getEastWest(); + } + + /** + * Return the location accuracy in meter. + * + * @return float + */ + public float getAccuracy() { + return accuracy; + } + + /** + * Return the longitude in degrees. + * + * @return double + */ + public double getLongitude() { + return longitude; + } + + /** + * Return the latitude in degrees. + * + * @return double + */ + public double getLatitude() { + return latitude; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index e87eaf92e..851114ef9 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java @@ -1,62 +1,184 @@ package fr.free.nrw.commons.location; +import android.Manifest; +import android.app.Activity; import android.content.Context; -import android.location.Criteria; +import android.content.pm.PackageManager; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import timber.log.Timber; public class LocationServiceManager implements LocationListener { + public static final int LOCATION_REQUEST = 1; - private String provider; + private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 1000; + private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10; + + private Context context; private LocationManager locationManager; - private LatLng latestLocation; - private Float latestLocationAccuracy; + private Location lastLocation; + private final List locationListeners = new CopyOnWriteArrayList<>(); + private boolean isLocationManagerRegistered = false; + /** + * Constructs a new instance of LocationServiceManager. + * + * @param context the context + */ public LocationServiceManager(Context context) { + this.context = context; this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - provider = locationManager.getBestProvider(new Criteria(), true); - } - - public LatLng getLatestLocation() { - return latestLocation; } /** - * Returns the accuracy of the location. The measurement is - * given as a radius in meter of 68 % confidence. + * Returns the current status of the GPS provider. * - * @return Float + * @return true if the GPS provider is enabled */ - public Float getLatestLocationAccuracy() { - return latestLocationAccuracy; + public boolean isProviderEnabled() { + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); } - /** Registers a LocationManager to listen for current location. + /** + * Returns whether the location permission is granted. + * + * @return true if the location permission is granted + */ + public boolean isLocationPermissionGranted() { + return ContextCompat.checkSelfPermission(context, + Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; + } + + /** + * Requests the location permission to be granted. + * + * @param activity the activity + */ + public void requestPermissions(Activity activity) { + if (activity.isFinishing()) { + return; + } + ActivityCompat.requestPermissions(activity, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, + LOCATION_REQUEST); + } + + public boolean isPermissionExplanationRequired(Activity activity) { + return !activity.isFinishing() && + ActivityCompat.shouldShowRequestPermissionRationale(activity, + Manifest.permission.ACCESS_FINE_LOCATION); + } + + public LatLng getLastLocation() { + if (lastLocation == null) { + return null; + } + return LatLng.from(lastLocation); + } + + /** + * Registers a LocationManager to listen for current location. */ public void registerLocationManager() { + if (!isLocationManagerRegistered) + isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) + && requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); + } + + /** + * Requests location updates from the specified provider. + * + * @param locationProvider the location provider + * @return true if successful + */ + private boolean requestLocationUpdatesFromProvider(String locationProvider) { try { - locationManager.requestLocationUpdates(provider, 400, 1, this); - Location location = locationManager.getLastKnownLocation(provider); - //Location works, just need to 'send' GPS coords - // via emulator extended controls if testing on emulator - Timber.d("Checking for location..."); - if (location != null) { - this.onLocationChanged(location); - } + locationManager.requestLocationUpdates(locationProvider, + MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS, + MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS, + this); + return true; } catch (IllegalArgumentException e) { Timber.e(e, "Illegal argument exception"); + return false; } catch (SecurityException e) { Timber.e(e, "Security exception"); + return false; } } - /** Unregisters location manager. + /** + * Returns whether a given location is better than the current best location. + * + * @param location the location to be tested + * @param currentBestLocation the current best location + * @return true if the given location is better + */ + protected boolean isBetterLocation(Location location, Location currentBestLocation) { + if (currentBestLocation == null) { + // A new location is always better than no location + return true; + } + + // Check whether the new location fix is newer or older + long timeDelta = location.getTime() - currentBestLocation.getTime(); + boolean isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS; + boolean isSignificantlyOlder = timeDelta < -MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS; + boolean isNewer = timeDelta > 0; + + // If it's been more than two minutes since the current location, use the new location + // because the user has likely moved + if (isSignificantlyNewer) { + return true; + // If the new location is more than two minutes older, it must be worse + } else if (isSignificantlyOlder) { + return false; + } + + // Check whether the new location fix is more or less accurate + int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); + boolean isLessAccurate = accuracyDelta > 0; + boolean isMoreAccurate = accuracyDelta < 0; + boolean isSignificantlyLessAccurate = accuracyDelta > 200; + + // Check if the old and new location are from the same provider + boolean isFromSameProvider = isSameProvider(location.getProvider(), + currentBestLocation.getProvider()); + + // Determine location quality using a combination of timeliness and accuracy + if (isMoreAccurate) { + return true; + } else if (isNewer && !isLessAccurate) { + return true; + } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) { + return true; + } + return false; + } + + /** + * Checks whether two providers are the same + */ + private boolean isSameProvider(String provider1, String provider2) { + if (provider1 == null) { + return provider2 == null; + } + return provider1.equals(provider2); + } + + /** + * Unregisters location manager. */ public void unregisterLocationManager() { + isLocationManagerRegistered = false; try { locationManager.removeUpdates(this); } catch (SecurityException e) { @@ -64,15 +186,34 @@ public class LocationServiceManager implements LocationListener { } } + /** + * Adds a new listener to the list of location listeners. + * + * @param listener the new listener + */ + public void addLocationListener(LocationUpdateListener listener) { + if (!locationListeners.contains(listener)) { + locationListeners.add(listener); + } + } + + /** + * Removes a listener from the list of location listeners. + * + * @param listener the listener to be removed + */ + public void removeLocationListener(LocationUpdateListener listener) { + locationListeners.remove(listener); + } + @Override public void onLocationChanged(Location location) { - double currentLatitude = location.getLatitude(); - double currentLongitude = location.getLongitude(); - latestLocationAccuracy = location.getAccuracy(); - Timber.d("Latitude: %f Longitude: %f Accuracy %f", - currentLatitude, currentLongitude, latestLocationAccuracy); - - latestLocation = new LatLng(currentLatitude, currentLongitude, latestLocationAccuracy); + if (isBetterLocation(location, lastLocation)) { + lastLocation = location; + for (LocationUpdateListener listener : locationListeners) { + listener.onLocationChanged(LatLng.from(lastLocation)); + } + } } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java new file mode 100644 index 000000000..69d3048a1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java @@ -0,0 +1,5 @@ +package fr.free.nrw.commons.location; + +public interface LocationUpdateListener { + void onLocationChanged(LatLng latLng); +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index fe68203ec..ecf3cedb1 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -6,7 +6,6 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; @@ -22,6 +21,9 @@ import java.util.ArrayList; import java.util.Date; import java.util.Locale; +import javax.inject.Inject; +import javax.inject.Provider; + import fr.free.nrw.commons.License; import fr.free.nrw.commons.LicenseList; import fr.free.nrw.commons.Media; @@ -29,11 +31,12 @@ import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.MediaWikiImageView; import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.ui.widget.CompatTextView; import timber.log.Timber; -public class MediaDetailFragment extends Fragment { +public class MediaDetailFragment extends CommonsDaggerSupportFragment { private boolean editable; private MediaDetailPagerFragment.MediaDetailProvider detailProvider; @@ -53,6 +56,9 @@ public class MediaDetailFragment extends Fragment { return mf; } + @Inject + Provider mediaDataExtractorProvider; + private MediaWikiImageView image; private MediaDetailSpacer spacer; private int initialListTop = 0; @@ -69,8 +75,8 @@ public class MediaDetailFragment extends Fragment { private boolean categoriesPresent = false; private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once! private ViewTreeObserver.OnScrollChangedListener scrollListener; - DataSetObserver dataObserver; - private AsyncTask detailFetchTask; + private DataSetObserver dataObserver; + private AsyncTask detailFetchTask; private LicenseList licenseList; @Override @@ -89,7 +95,7 @@ public class MediaDetailFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - detailProvider = (MediaDetailPagerFragment.MediaDetailProvider)getActivity(); + detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity(); if (savedInstanceState != null) { editable = savedInstanceState.getBoolean("editable"); @@ -150,7 +156,8 @@ public class MediaDetailFragment extends Fragment { return view; } - @Override public void onResume() { + @Override + public void onResume() { super.onResume(); Media media = detailProvider.getMediaAtPosition(index); if (media == null) { @@ -188,13 +195,13 @@ public class MediaDetailFragment extends Fragment { @Override protected void onPreExecute() { - extractor = new MediaDataExtractor(media.getFilename(), licenseList); + extractor = mediaDataExtractorProvider.get(); } @Override protected Boolean doInBackground(Void... voids) { try { - extractor.fetch(); + extractor.fetch(media.getFilename(), licenseList); return Boolean.TRUE; } catch (IOException e) { Timber.d(e); @@ -232,13 +239,13 @@ public class MediaDetailFragment extends Fragment { detailFetchTask.cancel(true); detailFetchTask = null; } - if (layoutListener != null) { + if (layoutListener != null && getView() != null) { getView().getViewTreeObserver().removeGlobalOnLayoutListener(layoutListener); // old Android was on crack. CRACK IS WHACK layoutListener = null; } - if (scrollListener != null) { + if (scrollListener != null && getView() != null) { getView().getViewTreeObserver().removeOnScrollChangedListener(scrollListener); - scrollListener = null; + scrollListener = null; } if (dataObserver != null) { detailProvider.unregisterDataSetObserver(dataObserver); @@ -283,7 +290,7 @@ public class MediaDetailFragment extends Fragment { private View buildCatLabel(final String catName, ViewGroup categoryContainer) { final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false); - final CompatTextView textView = (CompatTextView)item.findViewById(R.id.mediaDetailCategoryItemText); + final CompatTextView textView = (CompatTextView) item.findViewById(R.id.mediaDetailCategoryItemText); textView.setText(catName); if (categoriesLoaded && categoriesPresent) { @@ -302,7 +309,7 @@ public class MediaDetailFragment extends Fragment { // You must face the darkness alone int scrollY = scrollView.getScrollY(); int scrollMax = getView().getHeight(); - float scrollPercentage = (float)scrollY / (float)scrollMax; + float scrollPercentage = (float) scrollY / (float) scrollMax; final float transparencyMax = 0.75f; if (scrollPercentage > transparencyMax) { scrollPercentage = transparencyMax; @@ -356,7 +363,8 @@ public class MediaDetailFragment extends Fragment { } - private @Nullable String licenseLink(Media media) { + private @Nullable + String licenseLink(Media media) { String licenseKey = media.getLicense(); if (licenseKey == null || licenseKey.equals("")) { return null; @@ -377,7 +385,7 @@ public class MediaDetailFragment extends Fragment { private void openMap(LatLng coordinates) { //Open map app at given position Uri gmmIntentUri = Uri.parse( - "geo:0,0?q=" + coordinates.getLatitude() + "," + coordinates.getLatitude()); + "geo:0,0?q=" + coordinates.getLatitude() + "," + coordinates.getLongitude()); Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) { diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index 0b718df2a..3dd8d69e8 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -3,6 +3,7 @@ package fr.free.nrw.commons.media; import android.annotation.SuppressLint; import android.app.DownloadManager; import android.content.Intent; +import android.content.SharedPreferences; import android.database.DataSetObserver; import android.net.Uri; import android.os.Build; @@ -24,20 +25,31 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import fr.free.nrw.commons.CommonsApplication; +import javax.inject.Inject; +import javax.inject.Named; + import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.ContributionsActivity; -import fr.free.nrw.commons.mwapi.EventLog; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.content.Context.DOWNLOAD_SERVICE; import static android.content.Intent.ACTION_VIEW; import static android.content.pm.PackageManager.PERMISSION_GRANTED; -import static fr.free.nrw.commons.CommonsApplication.EVENT_SHARE_ATTEMPT; -public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPageChangeListener { +public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener { + + @Inject + MediaWikiApi mwApi; + @Inject + SessionManager sessionManager; + @Inject + @Named("default_preferences") + SharedPreferences prefs; private ViewPager pager; private Boolean editable; @@ -99,12 +111,7 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa Media m = provider.getMediaAtPosition(pager.getCurrentItem()); switch (item.getItemId()) { case R.id.menu_share_current_image: - // Share - this is just logs it, intent set in onCreateOptionsMenu, around line 252 - CommonsApplication app = (CommonsApplication) getActivity().getApplication(); - EventLog.schema(EVENT_SHARE_ATTEMPT) - .param("username", app.getCurrentAccount().name) - .param("filename", m.getFilename()) - .log(); + // Share - intent set in onCreateOptionsMenu, around line 252 return true; case R.id.menu_browser_current_image: // View in browser @@ -141,8 +148,14 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa private void downloadMedia(Media m) { String imageUrl = m.getImageUrl(), fileName = m.getFilename(); + + if (imageUrl == null || fileName == null) { + return; + } + // Strip 'File:' from beginning of filename, we really shouldn't store it fileName = fileName.replaceFirst("^File:", ""); + Uri imageUri = Uri.parse(imageUrl); DownloadManager.Request req = new DownloadManager.Request(imageUri); @@ -155,15 +168,19 @@ public class MediaDetailPagerFragment extends Fragment implements ViewPager.OnPa req.allowScanningByMediaScanner(); req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - && !(ContextCompat.checkSelfPermission(getContext(), - READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE) + != PERMISSION_GRANTED + && getView() != null) { Snackbar.make(getView(), R.string.read_storage_permission_rationale, Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok, view -> ActivityCompat.requestPermissions(getActivity(), new String[]{READ_EXTERNAL_STORAGE}, 1)).show(); } else { - ((DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE)).enqueue(req); + DownloadManager systemService = (DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE); + if (systemService != null) { + systemService.enqueue(req); + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/CategoryModifier.java b/app/src/main/java/fr/free/nrw/commons/modifications/CategoryModifier.java index bb650513b..556815a97 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/CategoryModifier.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/CategoryModifier.java @@ -13,7 +13,7 @@ public class CategoryModifier extends PageModifier { public CategoryModifier(String... categories) { super(MODIFIER_NAME); JSONArray categoriesArray = new JSONArray(); - for(String category: categories) { + for (String category: categories) { categoriesArray.put(category); } try { @@ -34,7 +34,7 @@ public class CategoryModifier extends PageModifier { categories = params.optJSONArray(PARAM_CATEGORIES); StringBuilder categoriesString = new StringBuilder(); - for(int i=0; i < categories.length(); i++) { + for (int i = 0; i < categories.length(); i++) { String category = categories.optString(i); categoriesString.append("\n[[Category:").append(category).append("]]"); } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java index 11caa94fa..0d4468d84 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons.modifications; -import android.content.ContentProvider; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; @@ -10,50 +9,51 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.text.TextUtils; -import fr.free.nrw.commons.CommonsApplication; +import javax.inject.Inject; + +import fr.free.nrw.commons.data.DBOpenHelper; +import fr.free.nrw.commons.di.CommonsDaggerContentProvider; import timber.log.Timber; -public class ModificationsContentProvider extends ContentProvider{ +import static fr.free.nrw.commons.modifications.ModifierSequenceDao.Table.TABLE_NAME; + +public class ModificationsContentProvider extends CommonsDaggerContentProvider { private static final int MODIFICATIONS = 1; private static final int MODIFICATIONS_ID = 2; - public static final String AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider"; - private static final String BASE_PATH = "modifications"; + public static final String MODIFICATIONS_AUTHORITY = "fr.free.nrw.commons.modifications.contentprovider"; + public static final String BASE_PATH = "modifications"; - public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH); + public static final Uri BASE_URI = Uri.parse("content://" + MODIFICATIONS_AUTHORITY + "/" + BASE_PATH); private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { - uriMatcher.addURI(AUTHORITY, BASE_PATH, MODIFICATIONS); - uriMatcher.addURI(AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID); + uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH, MODIFICATIONS); + uriMatcher.addURI(MODIFICATIONS_AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID); } public static Uri uriForId(int id) { return Uri.parse(BASE_URI.toString() + "/" + id); } - - @Override - public boolean onCreate() { - return false; - } + @Inject DBOpenHelper dbOpenHelper; @Override public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); - queryBuilder.setTables(ModifierSequence.Table.TABLE_NAME); + queryBuilder.setTables(TABLE_NAME); int uriType = uriMatcher.match(uri); - switch(uriType) { + switch (uriType) { case MODIFICATIONS: break; default: throw new IllegalArgumentException("Unknown URI" + uri); } - SQLiteDatabase db = CommonsApplication.getInstance().getDBOpenHelper().getReadableDatabase(); + SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); cursor.setNotificationUri(getContext().getContentResolver(), uri); @@ -69,11 +69,11 @@ public class ModificationsContentProvider extends ContentProvider{ @Override public Uri insert(@NonNull Uri uri, ContentValues contentValues) { int uriType = uriMatcher.match(uri); - SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase(); - long id = 0; + SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); + long id; switch (uriType) { case MODIFICATIONS: - id = sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, contentValues); + id = sqlDB.insert(TABLE_NAME, null, contentValues); break; default: throw new IllegalArgumentException("Unknown URI: " + uri); @@ -85,11 +85,11 @@ public class ModificationsContentProvider extends ContentProvider{ @Override public int delete(@NonNull Uri uri, String s, String[] strings) { int uriType = uriMatcher.match(uri); - SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase(); + SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); switch (uriType) { case MODIFICATIONS_ID: String id = uri.getLastPathSegment(); - sqlDB.delete(ModifierSequence.Table.TABLE_NAME, + sqlDB.delete(TABLE_NAME, "_id = ?", new String[] { id } ); @@ -103,13 +103,13 @@ public class ModificationsContentProvider extends ContentProvider{ public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { Timber.d("Hello, bulk insert! (ModificationsContentProvider)"); int uriType = uriMatcher.match(uri); - SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase(); + SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); sqlDB.beginTransaction(); switch (uriType) { case MODIFICATIONS: - for(ContentValues value: values) { + for (ContentValues value: values) { Timber.d("Inserting! %s", value); - sqlDB.insert(ModifierSequence.Table.TABLE_NAME, null, value); + sqlDB.insert(TABLE_NAME, null, value); } break; default: @@ -131,11 +131,11 @@ public class ModificationsContentProvider extends ContentProvider{ In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise. */ int uriType = uriMatcher.match(uri); - SQLiteDatabase sqlDB = CommonsApplication.getInstance().getDBOpenHelper().getWritableDatabase(); - int rowsUpdated = 0; + SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); + int rowsUpdated; switch (uriType) { case MODIFICATIONS: - rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME, + rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, selection, selectionArgs); @@ -144,9 +144,9 @@ public class ModificationsContentProvider extends ContentProvider{ int id = Integer.valueOf(uri.getLastPathSegment()); if (TextUtils.isEmpty(selection)) { - rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME, + rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, - ModifierSequence.Table.COLUMN_ID + " = ?", + ModifierSequenceDao.Table.COLUMN_ID + " = ?", new String[] { String.valueOf(id) } ); } else { throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID"); diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java index f2940ec74..5d716d738 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsSyncAdapter.java @@ -1,9 +1,6 @@ package fr.free.nrw.commons.modifications; import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; import android.content.Context; @@ -14,15 +11,24 @@ import android.os.RemoteException; import java.io.IOException; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.Utils; +import javax.inject.Inject; + +import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionsContentProvider; +import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { + @Inject MediaWikiApi mwApi; + @Inject ContributionDao contributionDao; + @Inject ModifierSequenceDao modifierSequenceDao; + @Inject + SessionManager sessionManager; + public ModificationsSyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); } @@ -30,6 +36,11 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { @Override public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) { // This code is fraught with possibilities of race conditions, but lalalalala I can't hear you! + ApplicationlessInjection + .getInstance(getContext() + .getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); Cursor allModifications; try { @@ -44,27 +55,17 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { return; } - String authCookie; - try { - authCookie = AccountManager.get(getContext()).blockingGetAuthToken(account, "", false); - } catch (OperationCanceledException | AuthenticatorException e) { - throw new RuntimeException(e); - } catch (IOException e) { + String authCookie = sessionManager.getAuthCookie(); + if (isNullOrWhiteSpace(authCookie)) { Timber.d("Could not authenticate :("); return; } - if (Utils.isNullOrWhiteSpace(authCookie)) { - Timber.d("Could not authenticate :("); - return; - } - - MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); - api.setAuthCookie(authCookie); + mwApi.setAuthCookie(authCookie); String editToken; try { - editToken = api.getEditToken(); + editToken = mwApi.getEditToken(); } catch (IOException e) { Timber.d("Can not retreive edit token!"); return; @@ -76,28 +77,36 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { ContentProviderClient contributionsClient = null; try { - contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY); + contributionsClient = getContext().getContentResolver().acquireContentProviderClient(ContributionsContentProvider.CONTRIBUTION_AUTHORITY); while (!allModifications.isAfterLast()) { - ModifierSequence sequence = ModifierSequence.fromCursor(allModifications); - sequence.setContentProviderClient(contentProviderClient); + ModifierSequence sequence = modifierSequenceDao.fromCursor(allModifications); Contribution contrib; - Cursor contributionCursor; + + if (contributionsClient == null) { + Timber.e("ContributionsClient is null. This should not happen!"); + return; + } + try { contributionCursor = contributionsClient.query(sequence.getMediaUri(), null, null, null, null); } catch (RemoteException e) { throw new RuntimeException(e); } - contributionCursor.moveToFirst(); - contrib = Contribution.fromCursor(contributionCursor); - if (contrib.getState() == Contribution.STATE_COMPLETED) { + if (contributionCursor != null) { + contributionCursor.moveToFirst(); + } + + contrib = contributionDao.fromCursor(contributionCursor); + + if (contrib != null && contrib.getState() == Contribution.STATE_COMPLETED) { String pageContent; try { - pageContent = api.revisionsByFilename(contrib.getFilename()); + pageContent = mwApi.revisionsByFilename(contrib.getFilename()); } catch (IOException e) { - Timber.d("Network fuckup on modifications sync!"); + Timber.d("Network messed up on modifications sync!"); continue; } @@ -106,19 +115,19 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { String editResult; try { - editResult = api.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary()); + editResult = mwApi.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary()); } catch (IOException e) { - Timber.d("Network fuckup on modifications sync!"); + Timber.d("Network messed up on modifications sync!"); continue; } Timber.d("Response is %s", editResult); - if (!editResult.equals("Success")) { + if (!"Success".equals(editResult)) { // FIXME: Log this somewhere else Timber.d("Non success result! %s", editResult); } else { - sequence.delete(); + modifierSequenceDao.delete(sequence); } } allModifications.moveToNext(); @@ -129,4 +138,8 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { } } } + + private boolean isNullOrWhiteSpace(String value) { + return value == null || value.trim().isEmpty(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java index 36012e55e..93cb3bc3d 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java @@ -1,14 +1,8 @@ package fr.free.nrw.commons.modifications; -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.net.Uri; -import android.os.RemoteException; import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; @@ -17,22 +11,21 @@ public class ModifierSequence { private Uri mediaUri; private ArrayList modifiers; private Uri contentUri; - private ContentProviderClient client; public ModifierSequence(Uri mediaUri) { this.mediaUri = mediaUri; modifiers = new ArrayList<>(); } - public ModifierSequence(Uri mediaUri, JSONObject data) { + ModifierSequence(Uri mediaUri, JSONObject data) { this(mediaUri); JSONArray modifiersJSON = data.optJSONArray("modifiers"); - for (int i=0; i< modifiersJSON.length(); i++) { + for (int i = 0; i < modifiersJSON.length(); i++) { modifiers.add(PageModifier.fromJSON(modifiersJSON.optJSONObject(i))); } } - public Uri getMediaUri() { + Uri getMediaUri() { return mediaUri; } @@ -40,113 +33,32 @@ public class ModifierSequence { modifiers.add(modifier); } - public String executeModifications(String pageName, String pageContents) { + String executeModifications(String pageName, String pageContents) { for (PageModifier modifier: modifiers) { pageContents = modifier.doModification(pageName, pageContents); } return pageContents; } - public String getEditSummary() { + String getEditSummary() { StringBuilder editSummary = new StringBuilder(); - for(PageModifier modifier: modifiers) { + for (PageModifier modifier: modifiers) { editSummary.append(modifier.getEditSumary()).append(" "); } editSummary.append("Via Commons Mobile App"); return editSummary.toString(); } - public JSONObject toJSON() { - JSONObject data = new JSONObject(); - try { - JSONArray modifiersJSON = new JSONArray(); - for (PageModifier modifier: modifiers) { - modifiersJSON.put(modifier.toJSON()); - } - data.put("modifiers", modifiersJSON); - return data; - } catch (JSONException e) { - throw new RuntimeException(e); - } + ArrayList getModifiers() { + return modifiers; } - public ContentValues toContentValues() { - ContentValues cv = new ContentValues(); - cv.put(Table.COLUMN_MEDIA_URI, mediaUri.toString()); - cv.put(Table.COLUMN_DATA, toJSON().toString()); - return cv; + Uri getContentUri() { + return contentUri; } - public static ModifierSequence fromCursor(Cursor cursor) { - // Hardcoding column positions! - ModifierSequence ms = null; - try { - ms = new ModifierSequence(Uri.parse(cursor.getString(1)), - new JSONObject(cursor.getString(2))); - } catch (JSONException e) { - throw new RuntimeException(e); - } - ms.contentUri = ModificationsContentProvider.uriForId(cursor.getInt(0)); - - return ms; + void setContentUri(Uri contentUri) { + this.contentUri = contentUri; } - public void save() { - try { - if(contentUri == null) { - contentUri = client.insert(ModificationsContentProvider.BASE_URI, this.toContentValues()); - } else { - client.update(contentUri, toContentValues(), null, null); - } - } catch(RemoteException e) { - throw new RuntimeException(e); - } - } - - public void delete() { - try { - client.delete(contentUri, null, null); - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - public void setContentProviderClient(ContentProviderClient client) { - this.client = client; - } - - public static class Table { - public static final String TABLE_NAME = "modifications"; - - public static final String COLUMN_ID = "_id"; - public static final String COLUMN_MEDIA_URI = "mediauri"; - public static final String COLUMN_DATA = "data"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_ID, - COLUMN_MEDIA_URI, - COLUMN_DATA - }; - - private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + "_id INTEGER PRIMARY KEY," - + "mediauri STRING," - + "data STRING" - + ");"; - - public static void onCreate(SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - public static void onUpdate(SQLiteDatabase db, int from, int to) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); - onCreate(db); - } - - public static void onDelete(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); - onCreate(db); - } - } } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java new file mode 100644 index 000000000..a23079b5e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java @@ -0,0 +1,124 @@ +package fr.free.nrw.commons.modifications; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.RemoteException; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; + +public class ModifierSequenceDao { + + private final Provider clientProvider; + + @Inject + public ModifierSequenceDao(@Named("modification") Provider clientProvider) { + this.clientProvider = clientProvider; + } + + public void save(ModifierSequence sequence) { + ContentProviderClient db = clientProvider.get(); + try { + if (sequence.getContentUri() == null) { + sequence.setContentUri(db.insert(ModificationsContentProvider.BASE_URI, toContentValues(sequence))); + } else { + db.update(sequence.getContentUri(), toContentValues(sequence), null, null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + db.release(); + } + } + + public void delete(ModifierSequence sequence) { + ContentProviderClient db = clientProvider.get(); + try { + db.delete(sequence.getContentUri(), null, null); + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + db.release(); + } + } + + ModifierSequence fromCursor(Cursor cursor) { + // Hardcoding column positions! + ModifierSequence ms; + try { + ms = new ModifierSequence(Uri.parse(cursor.getString(1)), + new JSONObject(cursor.getString(2))); + } catch (JSONException e) { + throw new RuntimeException(e); + } + ms.setContentUri( ModificationsContentProvider.uriForId(cursor.getInt(0))); + + return ms; + } + + private JSONObject toJSON(ModifierSequence sequence) { + JSONObject data = new JSONObject(); + try { + JSONArray modifiersJSON = new JSONArray(); + for (PageModifier modifier: sequence.getModifiers()) { + modifiersJSON.put(modifier.toJSON()); + } + data.put("modifiers", modifiersJSON); + return data; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + private ContentValues toContentValues(ModifierSequence sequence) { + ContentValues cv = new ContentValues(); + cv.put(Table.COLUMN_MEDIA_URI, sequence.getMediaUri().toString()); + cv.put(Table.COLUMN_DATA, toJSON(sequence).toString()); + return cv; + } + + public static class Table { + static final String TABLE_NAME = "modifications"; + + static final String COLUMN_ID = "_id"; + static final String COLUMN_MEDIA_URI = "mediauri"; + static final String COLUMN_DATA = "data"; + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + public static final String[] ALL_FIELDS = { + COLUMN_ID, + COLUMN_MEDIA_URI, + COLUMN_DATA + }; + + static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; + + static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" + + "_id INTEGER PRIMARY KEY," + + "mediauri STRING," + + "data STRING" + + ");"; + + public static void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_STATEMENT); + } + + public static void onUpdate(SQLiteDatabase db, int from, int to) { + db.execSQL(DROP_TABLE_STATEMENT); + onCreate(db); + } + + public static void onDelete(SQLiteDatabase db) { + db.execSQL(DROP_TABLE_STATEMENT); + onCreate(db); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/PageModifier.java b/app/src/main/java/fr/free/nrw/commons/modifications/PageModifier.java index f4b3c7359..0da25d9a6 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/PageModifier.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/PageModifier.java @@ -7,9 +7,9 @@ public abstract class PageModifier { public static PageModifier fromJSON(JSONObject data) { String name = data.optString("name"); - if(name.equals(CategoryModifier.MODIFIER_NAME)) { + if (name.equals(CategoryModifier.MODIFIER_NAME)) { return new CategoryModifier(data.optJSONObject("data")); - } else if(name.equals(TemplateRemoveModifier.MODIFIER_NAME)) { + } else if (name.equals(TemplateRemoveModifier.MODIFIER_NAME)) { return new TemplateRemoveModifier(data.optJSONObject("data")); } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/TemplateRemoveModifier.java b/app/src/main/java/fr/free/nrw/commons/modifications/TemplateRemoveModifier.java index 6149084c1..f9942b007 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/TemplateRemoveModifier.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/TemplateRemoveModifier.java @@ -41,18 +41,18 @@ public class TemplateRemoveModifier extends PageModifier { Pattern templateStartPattern = Pattern.compile("\\{\\{" + templateNormalized, Pattern.CASE_INSENSITIVE); Matcher matcher = templateStartPattern.matcher(pageContents); - while(matcher.find()) { + while (matcher.find()) { int braceCount = 1; int startIndex = matcher.start(); int curIndex = matcher.end(); Matcher openMatch = PATTERN_TEMPLATE_OPEN.matcher(pageContents); Matcher closeMatch = PATTERN_TEMPLATE_CLOSE.matcher(pageContents); - while(curIndex < pageContents.length()) { + while (curIndex < pageContents.length()) { boolean openFound = openMatch.find(curIndex); boolean closeFound = closeMatch.find(curIndex); - if(openFound && (!closeFound || openMatch.start() < closeMatch.start())) { + if (openFound && (!closeFound || openMatch.start() < closeMatch.start())) { braceCount++; curIndex = openMatch.end(); } else if (closeFound) { @@ -71,8 +71,8 @@ public class TemplateRemoveModifier extends PageModifier { } // Strip trailing whitespace - while(curIndex < pageContents.length()) { - if(pageContents.charAt(curIndex) == ' ' || pageContents.charAt(curIndex) == '\n') { + while (curIndex < pageContents.length()) { + if (pageContents.charAt(curIndex) == ' ' || pageContents.charAt(curIndex) == '\n') { curIndex++; } else { break; diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 4cbfe7c3a..fe9700ef5 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -1,5 +1,7 @@ package fr.free.nrw.commons.mwapi; +import android.content.Context; +import android.content.SharedPreferences; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -21,10 +23,14 @@ import org.apache.http.params.CoreProtocolPNames; import org.apache.http.util.EntityUtils; import org.mediawiki.api.ApiResult; import org.mediawiki.api.MWApi; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -34,12 +40,17 @@ import java.util.concurrent.Callable; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.PageTitle; -import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.notification.Notification; import in.yuvi.http.fluent.Http; import io.reactivex.Observable; import io.reactivex.Single; import timber.log.Timber; +import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN; +import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationFromApiResult; +import static fr.free.nrw.commons.notification.NotificationUtils.getNotificationType; +import static fr.free.nrw.commons.notification.NotificationUtils.isCommonsNotification; + /** * @author Addshore */ @@ -49,17 +60,27 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { private static final String THUMB_SIZE = "640"; private AbstractHttpClient httpClient; private MWApi api; + private Context context; + private SharedPreferences sharedPreferences; - public ApacheHttpClientMediaWikiApi(String apiURL) { + public ApacheHttpClientMediaWikiApi(Context context, String apiURL, SharedPreferences sharedPreferences) { + this.context = context; BasicHttpParams params = new BasicHttpParams(); SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory(); schemeRegistry.register(new Scheme("https", sslSocketFactory, 443)); ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry); - params.setParameter(CoreProtocolPNames.USER_AGENT, "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE); + params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent()); httpClient = new DefaultHttpClient(cm, params); api = new MWApi(apiURL, httpClient); + this.sharedPreferences = sharedPreferences; + } + + @Override + @NonNull + public String getUserAgent() { + return "Commons/" + BuildConfig.VERSION_NAME + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE; } @VisibleForTesting @@ -74,11 +95,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { * @throws IOException On api request IO issue */ public String login(String username, String password) throws IOException { + String loginToken = getLoginToken(); + Timber.d("Login token is %s", loginToken); return getErrorCodeToReturn(api.action("clientlogin") .param("rememberMe", "1") .param("username", username) .param("password", password) - .param("logintoken", getLoginToken()) + .param("logintoken", loginToken) .param("loginreturnurl", "https://commons.wikimedia.org") .post()); } @@ -91,12 +114,14 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { * @throws IOException On api request IO issue */ public String login(String username, String password, String twoFactorCode) throws IOException { + String loginToken = getLoginToken(); + Timber.d("Login token is %s", loginToken); return getErrorCodeToReturn(api.action("clientlogin") - .param("rememberMe", "1") + .param("rememberMe", "true") .param("username", username) .param("password", password) - .param("logintoken", getLoginToken()) - .param("logincontinue", "1") + .param("logintoken", loginToken) + .param("logincontinue", "true") .param("OATHToken", twoFactorCode) .post()); } @@ -121,14 +146,17 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { String status = loginApiResult.getString("/api/clientlogin/@status"); if (status.equals("PASS")) { api.isLoggedIn = true; + setAuthCookieOnLogin(true); return status; } else if (status.equals("FAIL")) { + setAuthCookieOnLogin(false); return loginApiResult.getString("/api/clientlogin/@messagecode"); } else if ( status.equals("UI") && loginApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest") && loginApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).") ) { + setAuthCookieOnLogin(false); return "2FA"; } @@ -136,6 +164,18 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { return "genericerror-" + status; } + private void setAuthCookieOnLogin(boolean isLoggedIn) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + if (isLoggedIn) { + editor.putBoolean("isUserLoggedIn", true); + editor.putString("getAuthCookie", api.getAuthCookie()); + } else { + editor.putBoolean("isUserLoggedIn", false); + editor.remove("getAuthCookie"); + } + editor.apply(); + } + @Override public String getAuthCookie() { return api.getAuthCookie(); @@ -335,7 +375,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { logEvents.add(new LogEventResult.LogEvent( image.getString("@pageid"), image.getString("@title"), - Utils.parseMWDate(image.getString("@timestamp"))) + parseMWDate(image.getString("@timestamp"))) ); } return logEvents; @@ -352,6 +392,42 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { .getString("/api/query/pages/page/revisions/rev"); } + @Override + @NonNull + public List getNotifications() { + ApiResult notificationNode = null; + try { + notificationNode = api.action("query") + .param("notprop", "list") + .param("format", "xml") + .param("meta", "notifications") + .param("notfilter", "!read") + .get() + .getNode("/api/query/notifications/list"); + } catch (IOException e) { + Timber.e("Failed to obtain searchCategories", e); + } + + if (notificationNode == null) { + return new ArrayList<>(); + } + + List notifications = new ArrayList<>(); + + NodeList childNodes = notificationNode.getDocument().getChildNodes(); + + for (int i = 0; i < childNodes.getLength(); i++) { + Node node = childNodes.item(i); + if (isCommonsNotification(node) + && !getNotificationType(node).equals(UNKNOWN)) { + notifications.add(getNotificationFromApiResult(context, node)); + } + } + + return notifications; + } + + @Override public boolean existingFile(String fileSha1) throws IOException { return api.action("query") @@ -387,17 +463,22 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { @Override @NonNull - public UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, final ProgressListener progressListener) throws IOException { + public UploadResult uploadFile(String filename, + @NonNull InputStream file, + long dataLength, + String pageContents, + String editSummary, + final ProgressListener progressListener) throws IOException { ApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, progressListener::onProgress); - Log.e("WTF", "Result: " +result.toString()); + Log.e("WTF", "Result: " + result.toString()); String resultStatus = result.getString("/api/upload/@result"); if (!resultStatus.equals("Success")) { String errorCode = result.getString("/api/error/@code"); return new UploadResult(resultStatus, errorCode); } else { - Date dateUploaded = Utils.parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); + Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); String canonicalFilename = "File:" + result.getString("/api/upload/@filename").replace("_", " "); // Title vs Filename String imageUrl = result.getString("/api/upload/imageinfo/@url"); return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl); @@ -423,4 +504,13 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { return Integer.parseInt(uploadCount); }); } + + private Date parseMWDate(String mwDate) { + SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC + try { + return isoFormat.parse(mwDate); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java b/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java index d3ba7c0d5..4446da738 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/EventLog.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.mwapi; +import android.content.SharedPreferences; import android.os.Build; import fr.free.nrw.commons.Utils; @@ -15,14 +16,14 @@ public class EventLog { } } - private static LogBuilder schema(String schema, long revision) { - return new LogBuilder(schema, revision); + private static LogBuilder schema(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) { + return new LogBuilder(schema, revision, mwApi, prefs); } - public static LogBuilder schema(Object[] scid) { + public static LogBuilder schema(Object[] scid, MediaWikiApi mwApi, SharedPreferences prefs) { if (scid.length != 2) { throw new IllegalArgumentException("Needs an object array with schema as first param and revision as second"); } - return schema((String) scid[0], (Long) scid[1]); + return schema((String) scid[0], (Long) scid[1], mwApi, prefs); } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java b/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java index eabbbf82e..2a2456cc3 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/LogBuilder.java @@ -3,7 +3,6 @@ package fr.free.nrw.commons.mwapi; import android.content.SharedPreferences; import android.os.AsyncTask; import android.os.Build; -import android.preference.PreferenceManager; import org.json.JSONException; import org.json.JSONObject; @@ -12,21 +11,39 @@ import java.net.MalformedURLException; import java.net.URL; import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.settings.Prefs; +@SuppressWarnings("WeakerAccess") public class LogBuilder { - private JSONObject data; - private long rev; - private String schema; + private final MediaWikiApi mwApi; + private final JSONObject data; + private final long rev; + private final String schema; + private final SharedPreferences prefs; - LogBuilder(String schema, long revision) { - data = new JSONObject(); + /** + * Main constructor of LogBuilder + * + * @param schema Log schema + * @param revision Log revision + * @param mwApi Wiki media API instance + * @param prefs Instance of SharedPreferences + */ + LogBuilder(String schema, long revision, MediaWikiApi mwApi, SharedPreferences prefs) { + this.prefs = prefs; + this.data = new JSONObject(); this.schema = schema; this.rev = revision; + this.mwApi = mwApi; } + /** + * Adds data to preferences + * @param key Log key + * @param value Log object value + * @return LogBuilder + */ public LogBuilder param(String key, Object value) { try { data.put(key, value); @@ -36,6 +53,10 @@ public class LogBuilder { return this; } + /** + * Encodes JSON object to URL + * @return URL to JSON object + */ URL toUrl() { JSONObject fullData = new JSONObject(); try { @@ -56,14 +77,13 @@ public class LogBuilder { // Use *only* for tracking the user preference change for EventLogging // Attempting to use anywhere else will cause kitten explosions public void log(boolean force) { - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance()); - if (!settings.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) { + if (!prefs.getBoolean(Prefs.TRACKING_ENABLED, true) && !force) { return; // User has disabled tracking } - LogTask logTask = new LogTask(); + LogTask logTask = new LogTask(mwApi); logTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this); } - + public void log() { log(false); } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java b/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java index ee947afbc..e564a50ab 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/LogTask.java @@ -2,11 +2,26 @@ package fr.free.nrw.commons.mwapi; import android.os.AsyncTask; -import fr.free.nrw.commons.CommonsApplication; - class LogTask extends AsyncTask { + + private final MediaWikiApi mwApi; + + /** + * Main constructor of LogTask + * + * @param mwApi Media wiki API instance + */ + public LogTask(MediaWikiApi mwApi) { + this.mwApi = mwApi; + } + + /** + * Logs events in background + * @param logBuilders LogBuilder instance + * @return Background success state ( TRUE or FALSE ) + */ @Override protected Boolean doInBackground(LogBuilder... logBuilders) { - return CommonsApplication.getInstance().getMWApi().logEvents(logBuilders); + return mwApi.logEvents(logBuilders); } } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaResult.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaResult.java index cfddf8c15..f2f34ce6d 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaResult.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaResult.java @@ -4,15 +4,29 @@ public class MediaResult { private final String wikiSource; private final String parseTreeXmlSource; + /** + * Full-fledged constructor of MediaResult + * + * @param wikiSource Media wiki source + * @param parseTreeXmlSource Media tree parsed in XML + */ MediaResult(String wikiSource, String parseTreeXmlSource) { this.wikiSource = wikiSource; this.parseTreeXmlSource = parseTreeXmlSource; } + /** + * Gets wiki source + * @return Wiki source + */ public String getWikiSource() { return wikiSource; } + /** + * Gets tree parsed in XML + * @return XML parsed tree + */ public String getParseTreeXmlSource() { return parseTreeXmlSource; } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index 310c97a8a..da9403b62 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -5,11 +5,15 @@ import android.support.annotation.Nullable; import java.io.IOException; import java.io.InputStream; +import java.util.List; +import fr.free.nrw.commons.notification.Notification; import io.reactivex.Observable; import io.reactivex.Single; public interface MediaWikiApi { + String getUserAgent(); + String getAuthCookie(); void setAuthCookie(String authCookie); @@ -43,6 +47,9 @@ public interface MediaWikiApi { @NonNull Observable allCategories(String filter, int searchCatsLimit); + @NonNull + List getNotifications() throws IOException; + @NonNull Observable searchTitles(String title, int searchCatsLimit); @@ -51,6 +58,8 @@ public interface MediaWikiApi { boolean existingFile(String fileSha1) throws IOException; + + @NonNull LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException; diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java b/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java index 34d050b2c..9422497b3 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/UploadResult.java @@ -9,11 +9,24 @@ public class UploadResult { private String imageUrl; private String canonicalFilename; + /** + * Minimal constructor + * + * @param resultStatus Upload result status + * @param errorCode Upload error code + */ UploadResult(String resultStatus, String errorCode) { this.resultStatus = resultStatus; this.errorCode = errorCode; } + /** + * Full-fledged constructor + * @param resultStatus Upload result status + * @param dateUploaded Uploaded date + * @param canonicalFilename Uploaded file name + * @param imageUrl Uploaded image file name + */ UploadResult(String resultStatus, Date dateUploaded, String canonicalFilename, String imageUrl) { this.resultStatus = resultStatus; this.dateUploaded = dateUploaded; @@ -21,22 +34,42 @@ public class UploadResult { this.imageUrl = imageUrl; } + /** + * Gets uploaded date + * @return Upload date + */ public Date getDateUploaded() { return dateUploaded; } + /** + * Gets image url + * @return Uploaded image url + */ public String getImageUrl() { return imageUrl; } + /** + * Gets canonical file name + * @return Uploaded file name + */ public String getCanonicalFilename() { return canonicalFilename; } + /** + * Gets upload error code + * @return Error code + */ public String getErrorCode() { return errorCode; } + /** + * Gets upload result status + * @return Upload result status + */ public String getResultStatus() { return resultStatus; } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java index d0322a187..0e67afc2f 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java @@ -1,21 +1,15 @@ package fr.free.nrw.commons.nearby; -import android.Manifest; -import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.location.LocationManager; import android.net.Uri; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; -import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; -import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.view.Menu; import android.view.MenuInflater; @@ -29,30 +23,43 @@ import com.google.gson.GsonBuilder; import java.util.List; +import javax.inject.Inject; + import butterknife.BindView; import butterknife.ButterKnife; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager; +import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.UriSerializer; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; import timber.log.Timber; -public class NearbyActivity extends NavigationBaseActivity { +public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener { - @BindView(R.id.progressBar) - ProgressBar progressBar; private static final int LOCATION_REQUEST = 1; private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed"; - private LocationServiceManager locationManager; + @BindView(R.id.progressBar) + ProgressBar progressBar; + + @Inject + LocationServiceManager locationManager; + @Inject + NearbyController nearbyController; + private LatLng curLatLang; private Bundle bundle; - private NearbyAsyncTask nearbyAsyncTask; private SharedPreferences sharedPreferences; private NearbyActivityMode viewMode; + private Disposable placesDisposable; + private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed @Override protected void onCreate(Bundle savedInstanceState) { @@ -60,7 +67,6 @@ public class NearbyActivity extends NavigationBaseActivity { sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); setContentView(R.layout.activity_nearby); ButterKnife.bind(this); - checkLocationPermission(); bundle = new Bundle(); initDrawer(); initViewState(); @@ -92,7 +98,8 @@ public class NearbyActivity extends NavigationBaseActivity { // Handle item selection switch (item.getItemId()) { case R.id.action_refresh: - refreshView(); + lockNearbyView(false); + refreshView(true); return true; case R.id.action_toggle_view: viewMode = viewMode.toggle(); @@ -104,60 +111,9 @@ public class NearbyActivity extends NavigationBaseActivity { } } - private void startLookingForNearby() { - locationManager = new LocationServiceManager(this); - locationManager.registerLocationManager(); - curLatLang = locationManager.getLatestLocation(); - nearbyAsyncTask = new NearbyAsyncTask(this); - nearbyAsyncTask.execute(); - } - - private void checkLocationPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (ContextCompat.checkSelfPermission(this, - Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { - startLookingForNearby(); - } else { - if (ContextCompat.checkSelfPermission(this, - Manifest.permission.ACCESS_FINE_LOCATION) - != PackageManager.PERMISSION_GRANTED) { - - // Should we show an explanation? - if (ActivityCompat.shouldShowRequestPermissionRationale(this, - Manifest.permission.ACCESS_FINE_LOCATION)) { - - // Show an explanation to the user *asynchronously* -- don't block - // this thread waiting for the user's response! After the user - // sees the explanation, try again to request the permission. - - new AlertDialog.Builder(this) - .setMessage(getString(R.string.location_permission_rationale)) - .setPositiveButton("OK", (dialog, which) -> { - ActivityCompat.requestPermissions(NearbyActivity.this, - new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, - LOCATION_REQUEST); - dialog.dismiss(); - }) - .setNegativeButton("Cancel", null) - .create() - .show(); - - } else { - - // No explanation needed, we can request the permission. - - ActivityCompat.requestPermissions(this, - new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, - LOCATION_REQUEST); - - // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an - // app-defined int constant. The callback method gets the - // result of the request. - } - } - } - } else { - startLookingForNearby(); + private void requestLocationPermissions() { + if (!isFinishing()) { + locationManager.requestPermissions(this); } } @@ -166,16 +122,10 @@ public class NearbyActivity extends NavigationBaseActivity { switch (requestCode) { case LOCATION_REQUEST: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - startLookingForNearby(); + refreshView(false); } else { //If permission not granted, go to page that says Nearby Places cannot be displayed - if (nearbyAsyncTask != null) { - nearbyAsyncTask.cancel(true); - } - if (progressBar != null) { - progressBar.setVisibility(View.GONE); - } - + hideProgressBar(); showLocationPermissionDeniedErrorDialog(); } } @@ -188,7 +138,7 @@ public class NearbyActivity extends NavigationBaseActivity { .setCancelable(false) .setPositiveButton(R.string.give_permission, (dialog, which) -> { //will ask for the location permission again - checkLocationPermission(); + checkGps(); }) .setNegativeButton(R.string.cancel, (dialog, which) -> { //dismiss dialog and finish activity @@ -200,8 +150,7 @@ public class NearbyActivity extends NavigationBaseActivity { } private void checkGps() { - LocationManager manager = (LocationManager) getSystemService(LOCATION_SERVICE); - if (!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + if (!locationManager.isProviderEnabled()) { Timber.d("GPS is not enabled"); new AlertDialog.Builder(this) .setMessage(R.string.gps_disabled) @@ -213,11 +162,48 @@ public class NearbyActivity extends NavigationBaseActivity { Timber.d("Loaded settings page"); startActivityForResult(callGPSSettingIntent, 1); }) - .setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> dialog.cancel()) + .setNegativeButton(R.string.menu_cancel_upload, (dialog, id) -> { + showLocationPermissionDeniedErrorDialog(); + dialog.cancel(); + }) .create() .show(); } else { Timber.d("GPS is enabled"); + checkLocationPermission(); + } + } + + private void checkLocationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (locationManager.isLocationPermissionGranted()) { + refreshView(false); + } else { + // Should we show an explanation? + if (locationManager.isPermissionExplanationRequired(this)) { + // Show an explanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + new AlertDialog.Builder(this) + .setMessage(getString(R.string.location_permission_rationale_nearby)) + .setPositiveButton("OK", (dialog, which) -> { + requestLocationPermissions(); + dialog.dismiss(); + }) + .setNegativeButton("Cancel", (dialog, id) -> { + showLocationPermissionDeniedErrorDialog(); + dialog.cancel(); + }) + .create() + .show(); + + } else { + // No explanation needed, we can request the permission. + requestLocationPermissions(); + } + } + } else { + refreshView(false); } } @@ -226,104 +212,122 @@ public class NearbyActivity extends NavigationBaseActivity { super.onActivityResult(requestCode, resultCode, data); if (requestCode == 1) { Timber.d("User is back from Settings page"); - refreshView(); + refreshView(false); } } private void toggleView() { - if (nearbyAsyncTask != null) { - if (nearbyAsyncTask.getStatus() == AsyncTask.Status.FINISHED) { - if (viewMode.isMap()) { - setMapFragment(); - } else { - setListFragment(); - } - } - sharedPreferences.edit().putBoolean(MAP_LAST_USED_PREFERENCE, viewMode.isMap()).apply(); + if (viewMode.isMap()) { + setMapFragment(); + } else { + setListFragment(); + } + sharedPreferences.edit().putBoolean(MAP_LAST_USED_PREFERENCE, viewMode.isMap()).apply(); + } + + @Override + protected void onStart() { + super.onStart(); + locationManager.addLocationListener(this); + } + + @Override + protected void onStop() { + super.onStop(); + locationManager.removeLocationListener(this); + locationManager.unregisterLocationManager(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (placesDisposable != null) { + placesDisposable.dispose(); } } @Override protected void onResume() { super.onResume(); + lockNearbyView = false; checkGps(); } - @Override - protected void onPause() { - super.onPause(); - if (nearbyAsyncTask != null) { - nearbyAsyncTask.cancel(true); + /** + * This method should be the single point to load/refresh nearby places + * + * @param isHardRefresh Should display a toast if the location hasn't changed + */ + private void refreshView(boolean isHardRefresh) { + if (lockNearbyView) { + return; } + locationManager.registerLocationManager(); + LatLng lastLocation = locationManager.getLastLocation(); + if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed + if (isHardRefresh) { + ViewUtil.showLongToast(this, R.string.nearby_location_has_not_changed); + } + return; + } + curLatLang = lastLocation; + + if (curLatLang == null) { + Timber.d("Skipping update of nearby places as location is unavailable"); + return; + } + + progressBar.setVisibility(View.VISIBLE); + placesDisposable = Observable.fromCallable(() -> nearbyController + .loadAttractionsFromLocation(curLatLang, this)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::populatePlaces); } - private void refreshView() { - nearbyAsyncTask = new NearbyAsyncTask(this); - nearbyAsyncTask.execute(); + private void populatePlaces(List placeList) { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Uri.class, new UriSerializer()) + .create(); + String gsonPlaceList = gson.toJson(placeList); + String gsonCurLatLng = gson.toJson(curLatLang); + + if (placeList.size() == 0) { + int duration = Toast.LENGTH_SHORT; + Toast toast = Toast.makeText(this, R.string.no_nearby, duration); + toast.show(); + } + + bundle.clear(); + bundle.putString("PlaceList", gsonPlaceList); + bundle.putString("CurLatLng", gsonCurLatLng); + + lockNearbyView(true); + // Begin the transaction + if (viewMode.isMap()) { + setMapFragment(); + } else { + setListFragment(); + } + + hideProgressBar(); } - @Override - protected void onDestroy() { - super.onDestroy(); - if (locationManager != null) { + private void lockNearbyView(boolean lock) { + if (lock) { + lockNearbyView = true; locationManager.unregisterLocationManager(); + locationManager.removeLocationListener(this); + } else { + lockNearbyView = false; + locationManager.registerLocationManager(); + locationManager.addLocationListener(this); } } - private class NearbyAsyncTask extends AsyncTask> { - - private final Context mContext; - - private NearbyAsyncTask(Context context) { - mContext = context; - } - - @Override - protected void onProgressUpdate(Integer... values) { - super.onProgressUpdate(values); - } - - @Override - protected List doInBackground(Void... params) { - return NearbyController - .loadAttractionsFromLocation(curLatLang, CommonsApplication.getInstance() - ); - } - - @Override - protected void onPostExecute(List placeList) { - super.onPostExecute(placeList); - - if (isCancelled()) { - return; - } - - Gson gson = new GsonBuilder() - .registerTypeAdapter(Uri.class, new UriSerializer()) - .create(); - String gsonPlaceList = gson.toJson(placeList); - String gsonCurLatLng = gson.toJson(curLatLang); - - if (placeList.size() == 0) { - int duration = Toast.LENGTH_SHORT; - Toast toast = Toast.makeText(mContext, R.string.no_nearby, duration); - toast.show(); - } - - bundle.clear(); - bundle.putString("PlaceList", gsonPlaceList); - bundle.putString("CurLatLng", gsonCurLatLng); - - // Begin the transaction - if (viewMode.isMap()) { - setMapFragment(); - } else { - setListFragment(); - } - - if (progressBar != null) { - progressBar.setVisibility(View.GONE); - } + private void hideProgressBar() { + if (progressBar != null) { + progressBar.setVisibility(View.GONE); } } @@ -334,7 +338,7 @@ public class NearbyActivity extends NavigationBaseActivity { FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); Fragment fragment = new NearbyMapFragment(); fragment.setArguments(bundle); - fragmentTransaction.replace(R.id.container, fragment); + fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName()); fragmentTransaction.commitAllowingStateLoss(); } @@ -345,12 +349,12 @@ public class NearbyActivity extends NavigationBaseActivity { FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); Fragment fragment = new NearbyListFragment(); fragment.setArguments(bundle); - fragmentTransaction.replace(R.id.container, fragment); + fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName()); fragmentTransaction.commitAllowingStateLoss(); } - public static void startYourself(Context context) { - Intent settingsIntent = new Intent(context, NearbyActivity.class); - context.startActivity(settingsIntent); + @Override + public void onLocationChanged(LatLng latLng) { + refreshView(false); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 58c8bb8a7..035532c11 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; -import android.preference.PreferenceManager; import android.support.graphics.drawable.VectorDrawableCompat; import com.mapbox.mapboxsdk.annotations.IconFactory; @@ -15,7 +14,9 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import fr.free.nrw.commons.CommonsApplication; +import javax.inject.Inject; +import javax.inject.Named; + import fr.free.nrw.commons.R; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.utils.UiUtils; @@ -24,45 +25,51 @@ import timber.log.Timber; import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween; import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; - public class NearbyController { private static final int MAX_RESULTS = 1000; + private final NearbyPlaces nearbyPlaces; + private final SharedPreferences prefs; + + @Inject + public NearbyController(NearbyPlaces nearbyPlaces, + @Named("default_preferences") SharedPreferences prefs) { + this.nearbyPlaces = nearbyPlaces; + this.prefs = prefs; + } /** * Prepares Place list to make their distance information update later. + * * @param curLatLng current location for user - * @param context context + * @param context context * @return Place list without distance information */ - public static List loadAttractionsFromLocation(LatLng curLatLng, Context context) { + public List loadAttractionsFromLocation(LatLng curLatLng, Context context) { Timber.d("Loading attractions near %s", curLatLng); if (curLatLng == null) { return Collections.emptyList(); } - NearbyPlaces nearbyPlaces = CommonsApplication.getInstance().getNearbyPlaces(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); List places = prefs.getBoolean("useWikidata", true) ? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()) : nearbyPlaces.getFromWikiNeedsPictures(); - if (curLatLng != null) { - Timber.d("Sorting places by distance..."); - final Map distances = new HashMap<>(); - for (Place place: places) { - distances.put(place, computeDistanceBetween(place.location, curLatLng)); - } - Collections.sort(places, - (lhs, rhs) -> { - double lhsDistance = distances.get(lhs); - double rhsDistance = distances.get(rhs); - return (int) (lhsDistance - rhsDistance); - } - ); + Timber.d("Sorting places by distance..."); + final Map distances = new HashMap<>(); + for (Place place : places) { + distances.put(place, computeDistanceBetween(place.location, curLatLng)); } + Collections.sort(places, + (lhs, rhs) -> { + double lhsDistance = distances.get(lhs); + double rhsDistance = distances.get(rhs); + return (int) (lhsDistance - rhsDistance); + } + ); return places; } /** * Loads attractions from location for list view, we need to return Place data type. + * * @param curLatLng users current location * @param placeList list of nearby places in Place data type * @return Place list that holds nearby places @@ -71,7 +78,7 @@ public class NearbyController { LatLng curLatLng, List placeList) { placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS)); - for (Place place: placeList) { + for (Place place : placeList) { String distance = formatDistanceBetween(curLatLng, place.location); place.setDistance(distance); } @@ -79,7 +86,8 @@ public class NearbyController { } /** - *Loads attractions from location for map view, we need to return BaseMarkerOption data type. + * Loads attractions from location for map view, we need to return BaseMarkerOption data type. + * * @param curLatLng users current location * @param placeList list of nearby places in Place data type * @return BaseMarkerOptions list that holds nearby places @@ -96,26 +104,28 @@ public class NearbyController { placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS)); - Bitmap icon = UiUtils.getBitmap( - VectorDrawableCompat.create( - context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme() - )); + VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create( + context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme() + ); + if (vectorDrawable != null) { + Bitmap icon = UiUtils.getBitmap(vectorDrawable); - for (Place place: placeList) { - String distance = formatDistanceBetween(curLatLng, place.location); - place.setDistance(distance); + for (Place place : placeList) { + String distance = formatDistanceBetween(curLatLng, place.location); + place.setDistance(distance); - NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker(); - nearbyBaseMarker.title(place.name); - nearbyBaseMarker.position( - new com.mapbox.mapboxsdk.geometry.LatLng( - place.location.getLatitude(), - place.location.getLongitude())); - nearbyBaseMarker.place(place); - nearbyBaseMarker.icon(IconFactory.getInstance(context) - .fromBitmap(icon)); + NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker(); + nearbyBaseMarker.title(place.name); + nearbyBaseMarker.position( + new com.mapbox.mapboxsdk.geometry.LatLng( + place.location.getLatitude(), + place.location.getLongitude())); + nearbyBaseMarker.place(place); + nearbyBaseMarker.icon(IconFactory.getInstance(context) + .fromBitmap(icon)); - baseMarkerOptions.add(nearbyBaseMarker); + baseMarkerOptions.add(nearbyBaseMarker); + } } return baseMarkerOptions; } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java index 2e677f33d..b383fd9b9 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java @@ -109,7 +109,7 @@ public class NearbyInfoDialog extends OverlayDialog { NearbyInfoDialog mDialog = new NearbyInfoDialog(); Bundle bundle = new Bundle(); bundle.putString(ARG_TITLE, place.name); - bundle.putString(ARG_DESC, place.getDescription().getText()); + bundle.putString(ARG_DESC, place.getLongDescription()); bundle.putDouble(ARG_LATITUDE, place.location.getLatitude()); bundle.putDouble(ARG_LONGITUDE, place.location.getLongitude()); bundle.putParcelable(ARG_SITE_LINK, place.siteLinks); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java index 00b8a2840..d57a23137 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.nearby; +import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; @@ -17,6 +18,7 @@ import java.lang.reflect.Type; import java.util.Collections; import java.util.List; +import dagger.android.support.AndroidSupportInjection; import fr.free.nrw.commons.R; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.utils.UriDeserializer; @@ -40,6 +42,12 @@ public class NearbyListFragment extends Fragment { setRetainInstance(true); } + @Override + public void onAttach(Context context) { + AndroidSupportInjection.inject(this); + super.onAttach(context); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -60,7 +68,7 @@ public class NearbyListFragment extends Fragment { Bundle bundle = this.getArguments(); if (bundle != null) { - String gsonPlaceList = bundle.getString("PlaceList"); + String gsonPlaceList = bundle.getString("PlaceList", "[]"); placeList = gson.fromJson(gsonPlaceList, LIST_TYPE); String gsonLatLng = bundle.getString("CurLatLng"); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java index d9e1aa633..535198699 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -3,7 +3,6 @@ package fr.free.nrw.commons.nearby; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; -import android.preference.PreferenceManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -77,6 +76,8 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment { private void setupMapView(Bundle savedInstanceState) { MapboxMapOptions options = new MapboxMapOptions() .styleUrl(Style.OUTDOORS) + .logoEnabled(false) + .attributionEnabled(false) .camera(new CameraPosition.Builder() .target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude())) .zoom(11) @@ -99,11 +100,8 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment { addCurrentLocationMarker(mapboxMap); }); - if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme",false)) { - mapView.setStyleUrl(getResources().getString(R.string.map_theme_dark)); - } else { - mapView.setStyleUrl(getResources().getString(R.string.map_theme_light)); - } + + mapView.setStyleUrl("asset://mapstyle.json"); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index 50d661ef6..f13aa1ae3 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -34,7 +34,7 @@ public class NearbyPlaces { public NearbyPlaces() { try { - wikidataQuery = FileUtils.readFromResource("/assets/queries/nearby_query.rq"); + wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq"); Timber.v(wikidataQuery); } catch (IOException e) { throw new RuntimeException(e); @@ -46,7 +46,7 @@ public class NearbyPlaces { try { // increase the radius gradually to find a satisfactory number of nearby places - while (radius < MAX_RADIUS) { + while (radius <= MAX_RADIUS) { places = getFromWikidataQuery(curLatLng, lang, radius); Timber.d("%d results at radius: %f", places.size(), radius); if (places.size() >= MIN_RESULTS) { @@ -62,6 +62,11 @@ public class NearbyPlaces { Timber.d("back to initial radius: %f", radius); radius = INITIAL_RADIUS; } + // make sure we will be able to send at least one request next time + if (radius > MAX_RADIUS) { + radius = MAX_RADIUS; + } + return places; } @@ -121,7 +126,7 @@ public class NearbyPlaces { places.add(new Place( name, - Place.Description.fromText(type), // list + Place.Label.fromText(type), // list type, // details Uri.parse(icon), new LatLng(latitude, longitude, 0), @@ -183,7 +188,7 @@ public class NearbyPlaces { places.add(new Place( name, - Place.Description.fromText(type), // list + Place.Label.fromText(type), // list type, // details null, new LatLng(latitude, longitude, 0), diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NoPermissionsFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NoPermissionsFragment.java index 5065a7d93..f08fa6acd 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NoPermissionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NoPermissionsFragment.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.nearby; +import android.content.Context; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; @@ -7,6 +8,7 @@ import android.view.View; import android.view.ViewGroup; import butterknife.ButterKnife; +import dagger.android.support.AndroidSupportInjection; import fr.free.nrw.commons.R; import timber.log.Timber; @@ -18,6 +20,12 @@ public class NoPermissionsFragment extends Fragment { public NoPermissionsFragment() { } + @Override + public void onAttach(Context context) { + AndroidSupportInjection.inject(this); + super.onAttach(context); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java index 428fcf6de..e8290c6e2 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java @@ -13,7 +13,7 @@ import fr.free.nrw.commons.location.LatLng; public class Place { public final String name; - private final Description description; + private final Label label; private final String longDescription; private final Uri secondaryImageUrl; public final LatLng location; @@ -24,18 +24,22 @@ public class Place { public final Sitelinks siteLinks; - public Place(String name, Description description, String longDescription, + public Place(String name, Label label, String longDescription, Uri secondaryImageUrl, LatLng location, Sitelinks siteLinks) { this.name = name; - this.description = description; + this.label = label; this.longDescription = longDescription; this.secondaryImageUrl = secondaryImageUrl; this.location = location; this.siteLinks = siteLinks; } - public Description getDescription() { - return description; + public Label getLabel() { + return label; + } + + public String getLongDescription() { + return longDescription; } public void setDistance(String distance) { @@ -67,10 +71,8 @@ public class Place { * Most common types of desc: building, house, cottage, farmhouse, * village, civil parish, church, railway station, * gatehouse, milestone, inn, secondary school, hotel - * - * TODO Give a more accurate class name (see issue #742). */ - public enum Description { + public enum Label { BUILDING("building", R.drawable.round_icon_generic_building), HOUSE("house", R.drawable.round_icon_house), @@ -95,19 +97,19 @@ public class Place { WATERFALL("waterfall", R.drawable.round_icon_waterfall), UNKNOWN("?", R.drawable.round_icon_unknown); - private static final Map TEXT_TO_DESCRIPTION - = new HashMap<>(Description.values().length); + private static final Map TEXT_TO_DESCRIPTION + = new HashMap<>(Label.values().length); static { - for (Description description : values()) { - TEXT_TO_DESCRIPTION.put(description.text, description); + for (Label label : values()) { + TEXT_TO_DESCRIPTION.put(label.text, label); } } private final String text; @DrawableRes private final int icon; - Description(String text, @DrawableRes int icon) { + Label(String text, @DrawableRes int icon) { this.text = text; this.icon = icon; } @@ -121,9 +123,9 @@ public class Place { return icon; } - public static Description fromText(String text) { - Description description = TEXT_TO_DESCRIPTION.get(text); - return description == null ? UNKNOWN : description; + public static Label fromText(String text) { + Label label = TEXT_TO_DESCRIPTION.get(text); + return label == null ? UNKNOWN : label; } } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java index f4c8a5d61..cf596e36e 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java @@ -43,13 +43,13 @@ class PlaceRenderer extends Renderer { public void render() { Place place = getContent(); tvName.setText(place.name); - String descriptionText = place.getDescription().getText(); + String descriptionText = place.getLongDescription(); if (descriptionText.equals("?")) { descriptionText = getContext().getString(R.string.no_description_found); } tvDesc.setText(descriptionText); distance.setText(place.distance); - icon.setImageResource(place.getDescription().getIcon()); + icon.setImageResource(place.getLabel().getIcon()); } interface PlaceClickedListener { diff --git a/app/src/main/java/fr/free/nrw/commons/notification/MarkReadResponse.java b/app/src/main/java/fr/free/nrw/commons/notification/MarkReadResponse.java new file mode 100644 index 000000000..03cdd6f88 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/MarkReadResponse.java @@ -0,0 +1,16 @@ +package fr.free.nrw.commons.notification; + +import android.support.annotation.Nullable; + +public class MarkReadResponse { + @SuppressWarnings("unused") @Nullable + private String result; + + public String result() { + return result; + } + + public static class QueryMarkReadResponse { + @SuppressWarnings("unused") @Nullable private MarkReadResponse echomarkread; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java new file mode 100644 index 000000000..e4efbff6c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java @@ -0,0 +1,21 @@ +package fr.free.nrw.commons.notification; + +/** + * Created by root on 18.12.2017. + */ + +public class Notification { + public NotificationType notificationType; + public String notificationText; + public String date; + public String description; + public String link; + + public Notification(NotificationType notificationType, String notificationText, String date, String description, String link) { + this.notificationType = notificationType; + this.notificationText = notificationText; + this.date = date; + this.description = description; + this.link = link; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java new file mode 100644 index 000000000..dc9d733f4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -0,0 +1,86 @@ +package fr.free.nrw.commons.notification; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; + +import com.pedrogomez.renderers.RVRendererAdapter; + +import java.util.List; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.theme.NavigationBaseActivity; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +/** + * Created by root on 18.12.2017. + */ + +public class NotificationActivity extends NavigationBaseActivity { + NotificationAdapterFactory notificationAdapterFactory; + + @BindView(R.id.listView) RecyclerView recyclerView; + + @Inject NotificationController controller; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_notification); + ButterKnife.bind(this); + initListView(); + initDrawer(); + } + + private void initListView() { + recyclerView = findViewById(R.id.listView); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + addNotifications(); + } + + @SuppressLint("CheckResult") + private void addNotifications() { + Timber.d("Add notifications"); + + Observable.fromCallable(() -> controller.getNotifications()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(notificationList -> { + Timber.d("Number of notifications is %d", notificationList.size()); + setAdapter(notificationList); + }, throwable -> Timber.e(throwable, "Error occurred while loading notifications")); + } + + private void handleUrl(String url) { + if (url == null || url.equals("")) { + return; + } + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); + } + + private void setAdapter(List notificationList) { + notificationAdapterFactory = new NotificationAdapterFactory(notification -> { + Timber.d("Notification clicked %s", notification.link); + handleUrl(notification.link); + }); + RVRendererAdapter adapter = notificationAdapterFactory.create(notificationList); + recyclerView.setAdapter(adapter); + } + + public static void startYourself(Context context) { + Intent intent = new Intent(context, NotificationActivity.class); + context.startActivity(intent); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapterFactory.java new file mode 100644 index 000000000..83a60dcfb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapterFactory.java @@ -0,0 +1,30 @@ +package fr.free.nrw.commons.notification; + +import android.support.annotation.NonNull; + +import com.pedrogomez.renderers.ListAdapteeCollection; +import com.pedrogomez.renderers.RVRendererAdapter; +import com.pedrogomez.renderers.RendererBuilder; + +import java.util.Collections; +import java.util.List; + +/** + * Created by root on 19.12.2017. + */ + +class NotificationAdapterFactory { + private NotificationRenderer.NotificationClicked listener; + + NotificationAdapterFactory(@NonNull NotificationRenderer.NotificationClicked listener) { + this.listener = listener; + } + + public RVRendererAdapter create(List notifications) { + RendererBuilder builder = new RendererBuilder() + .bind(Notification.class, new NotificationRenderer(listener)); + ListAdapteeCollection collection = new ListAdapteeCollection<>( + notifications != null ? notifications : Collections.emptyList()); + return new RVRendererAdapter<>(builder, collection); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java new file mode 100644 index 000000000..b22bafbb5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.notification; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.mwapi.MediaWikiApi; + +/** + * Created by root on 19.12.2017. + */ +@Singleton +public class NotificationController { + + private MediaWikiApi mediaWikiApi; + private SessionManager sessionManager; + + @Inject + public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) { + this.mediaWikiApi = mediaWikiApi; + this.sessionManager = sessionManager; + } + + public List getNotifications() throws IOException { + if (mediaWikiApi.validateLogin()) { + return mediaWikiApi.getNotifications(); + } else { + Boolean authTokenValidated = sessionManager.revalidateAuthToken(); + if (authTokenValidated != null && authTokenValidated) { + return mediaWikiApi.getNotifications(); + } + } + return new ArrayList<>(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java new file mode 100644 index 000000000..a5aac0508 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java @@ -0,0 +1,64 @@ +package fr.free.nrw.commons.notification; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.pedrogomez.renderers.Renderer; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; + +/** + * Created by root on 19.12.2017. + */ + +public class NotificationRenderer extends Renderer { + @BindView(R.id.title) TextView title; + @BindView(R.id.description) TextView description; + @BindView(R.id.time) TextView time; + @BindView(R.id.icon) ImageView icon; + private NotificationClicked listener; + + + NotificationRenderer(NotificationClicked listener) { + this.listener = listener; + } + + @Override + protected void setUpView(View view) { } + + @Override + protected void hookListeners(View rootView) { + rootView.setOnClickListener(v -> listener.notificationClicked(getContent())); + } + + @Override + protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) { + View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false); + ButterKnife.bind(this, inflatedView); + return inflatedView; + } + + @Override + public void render() { + Notification notification = getContent(); + title.setText(notification.notificationText); + time.setText(notification.date); + description.setText(notification.description); + switch (notification.notificationType) { + case THANK_YOU_EDIT: + icon.setImageResource(R.drawable.ic_edit_black_24dp); + break; + default: + icon.setImageResource(R.drawable.round_icon_unknown); + } + } + + public interface NotificationClicked{ + void notificationClicked(Notification notification); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationType.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationType.java new file mode 100644 index 000000000..b83b23b2a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationType.java @@ -0,0 +1,27 @@ +package fr.free.nrw.commons.notification; + +public enum NotificationType { + THANK_YOU_EDIT("thank-you-edit"), + EDIT_USER_TALK("edit-user-talk"), + MENTION("mention"), + WELCOME("welcome"), + UNKNOWN("unknown"); + private String type; + + NotificationType(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public static NotificationType handledValueOf(String name) { + for (NotificationType e : values()) { + if (e.getType().equals(name)) { + return e; + } + } + return UNKNOWN; + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java new file mode 100644 index 000000000..7f32da126 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java @@ -0,0 +1,116 @@ +package fr.free.nrw.commons.notification; + +import android.content.Context; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.annotation.Nullable; + +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.R; + +public class NotificationUtils { + + private static final String COMMONS_WIKI = "commonswiki"; + + public static boolean isCommonsNotification(Node document) { + if (document == null || !document.hasAttributes()) { + return false; + } + Element element = (Element) document; + return COMMONS_WIKI.equals(element.getAttribute("wiki")); + } + + public static NotificationType getNotificationType(Node document) { + Element element = (Element) document; + String type = element.getAttribute("type"); + return NotificationType.handledValueOf(type); + } + + public static Notification getNotificationFromApiResult(Context context, Node document) { + NotificationType type = getNotificationType(document); + + String notificationText = ""; + String link = getNotificationLink(document); + String description = getNotificationDescription(document); + switch (type) { + case THANK_YOU_EDIT: + notificationText = context.getString(R.string.notifications_thank_you_edit); + break; + case EDIT_USER_TALK: + notificationText = getUserTalkMessage(context, document); + break; + case MENTION: + notificationText = getMentionMessage(context, document); + break; + case WELCOME: + notificationText = getWelcomeMessage(context, document); + break; + } + return new Notification(type, notificationText, getTimestamp(document), description, link); + } + + public static String getMentionMessage(Context context, Node document) { + String format = context.getString(R.string.notifications_mention); + return String.format(format, getAgent(document), getNotificationDescription(document)); + } + + public static String getUserTalkMessage(Context context, Node document) { + String format = context.getString(R.string.notifications_talk_page_message); + return String.format(format, getAgent(document)); + } + + public static String getWelcomeMessage(Context context, Node document) { + String welcomeMessageFormat = context.getString(R.string.notifications_welcome); + return String.format(welcomeMessageFormat, getAgent(document)); + } + + private static String getAgent(Node document) { + Element agentElement = (Element) getNode(document, "agent"); + if (agentElement != null) { + return agentElement.getAttribute("name"); + } + return ""; + } + + private static String getTimestamp(Node document) { + Element timestampElement = (Element) getNode(document, "timestamp"); + if (timestampElement != null) { + return timestampElement.getAttribute("date"); + } + return ""; + } + + private static String getNotificationLink(Node document) { + String format = "%s%s"; + Element titleElement = (Element) getNode(document, "title"); + if (titleElement != null) { + String fullName = titleElement.getAttribute("full"); + return String.format(format, BuildConfig.HOME_URL, fullName); + } + return ""; + } + + private static String getNotificationDescription(Node document) { + Element titleElement = (Element) getNode(document, "title"); + if (titleElement != null) { + return titleElement.getAttribute("text"); + } + return ""; + } + + @Nullable + public static Node getNode(Node node, String nodeName) { + NodeList childNodes = node.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node nodeItem = childNodes.item(i); + Element item = (Element) nodeItem; + if (item.getTagName().equals(nodeName)) { + return nodeItem; + } + } + return null; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java index 5814ec904..54b462097 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java @@ -1,62 +1,70 @@ -package fr.free.nrw.commons.settings; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.support.v7.app.AppCompatDelegate; -import android.view.MenuItem; - -import butterknife.ButterKnife; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.theme.NavigationBaseActivity; - -public class SettingsActivity extends NavigationBaseActivity { - private AppCompatDelegate settingsDelegate; - - @Override - protected void onCreate(Bundle savedInstanceState) { - // Check prefs on every activity starts - if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false)) { - setTheme(R.style.DarkAppTheme); - } else { - setTheme(R.style.LightAppTheme); - } - - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_settings); - - ButterKnife.bind(this); - initDrawer(); - } - - // Get an action bar - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - if (settingsDelegate == null) { - settingsDelegate = AppCompatDelegate.create(this, null); - } - settingsDelegate.onPostCreate(savedInstanceState); - - //Get an up button - //settingsDelegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - //Handle action-bar clicks - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - public static void startYourself(Context context) { - Intent settingsIntent = new Intent(context, SettingsActivity.class); - context.startActivity(settingsIntent); - } +package fr.free.nrw.commons.settings; + +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v7.app.AppCompatDelegate; +import android.view.MenuItem; + +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.theme.NavigationBaseActivity; + +/** + * allows the user to change the settings + */ +public class SettingsActivity extends NavigationBaseActivity { + private AppCompatDelegate settingsDelegate; + + /** + * to be called when the activity starts + * @param savedInstanceState the previously saved state + */ + @Override + protected void onCreate(Bundle savedInstanceState) { + // Check prefs on every activity starts + if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false)) { + setTheme(R.style.DarkAppTheme); + } else { + setTheme(R.style.LightAppTheme); + } + + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + + ButterKnife.bind(this); + initDrawer(); + } + + // Get an action bar + /** + * takes care of actions taken after the creation has happened + * @param savedInstanceState the saved state + */ + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + if (settingsDelegate == null) { + settingsDelegate = AppCompatDelegate.create(this, null); + } + settingsDelegate.onPostCreate(savedInstanceState); + + //Get an up button + //settingsDelegate.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + /** + * Handle action-bar clicks + * @param item the selected item + * @return true on success, false on failure + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 0640ea444..66be88e3f 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -1,22 +1,50 @@ package fr.free.nrw.commons.settings; +import android.Manifest; import android.app.AlertDialog; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; import android.preference.ListPreference; +import android.preference.Preference; import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.FileProvider; +import android.widget.Toast; +import java.io.File; + +import javax.inject.Inject; +import javax.inject.Named; + +import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.utils.FileUtils; public class SettingsFragment extends PreferenceFragment { + + private static final int REQUEST_CODE_WRITE_EXTERNAL_STORAGE = 100; + + @Inject @Named("default_preferences") SharedPreferences prefs; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + ApplicationlessInjection + .getInstance(getActivity().getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); // Load the preferences from an XML resource addPreferencesFromResource(R.xml.preferences); @@ -38,14 +66,17 @@ public class SettingsFragment extends PreferenceFragment { }); final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads"); - final SharedPreferences sharedPref = PreferenceManager - .getDefaultSharedPreferences(CommonsApplication.getInstance()); - int uploads = sharedPref.getInt(Prefs.UPLOADS_SHOWING, 100); + int uploads = prefs.getInt(Prefs.UPLOADS_SHOWING, 100); uploadLimit.setText(uploads + ""); uploadLimit.setSummary(uploads + ""); uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> { - int value = Integer.parseInt(newValue.toString()); - final SharedPreferences.Editor editor = sharedPref.edit(); + int value; + try { + value = Integer.parseInt(newValue.toString()); + } catch(Exception e) { + value = 100; //Default number + } + final SharedPreferences.Editor editor = prefs.edit(); if (value > 500) { new AlertDialog.Builder(getActivity()) .setTitle(R.string.maximum_limit) @@ -58,14 +89,71 @@ public class SettingsFragment extends PreferenceFragment { uploadLimit.setSummary(500 + ""); uploadLimit.setText(500 + ""); } else { - editor.putInt(Prefs.UPLOADS_SHOWING, Integer.parseInt(newValue.toString())); + editor.putInt(Prefs.UPLOADS_SHOWING, value); editor.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED,true); - uploadLimit.setSummary(newValue.toString()); + uploadLimit.setSummary(String.valueOf(value)); } editor.apply(); return true; }); + Preference sendLogsPreference = findPreference("sendLogFile"); + sendLogsPreference.setOnPreferenceClickListener(preference -> { + //first we need to check if we have the necessary permissions + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission( + getActivity(), + Manifest.permission.WRITE_EXTERNAL_STORAGE) + == + PackageManager.PERMISSION_GRANTED) { + sendAppLogsViaEmail(); + } else { + //first get the necessary permission + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + REQUEST_CODE_WRITE_EXTERNAL_STORAGE); + } + } else { + sendAppLogsViaEmail(); + } + return true; + }); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == REQUEST_CODE_WRITE_EXTERNAL_STORAGE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + sendAppLogsViaEmail(); + } + } + } + + private void sendAppLogsViaEmail() { + String appLogs = Utils.getAppLogs(); + File appLogsFile = FileUtils.createAndGetAppLogsFile(appLogs); + + Context applicationContext = getActivity().getApplicationContext(); + Uri appLogsFilePath = FileProvider.getUriForFile( + getActivity(), + applicationContext.getPackageName() + ".provider", + appLogsFile + ); + + Intent feedbackIntent = new Intent(Intent.ACTION_SEND); + feedbackIntent.setType("message/rfc822"); + feedbackIntent.putExtra(Intent.EXTRA_EMAIL, + new String[]{CommonsApplication.LOGS_PRIVATE_EMAIL}); + feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, + String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT, + BuildConfig.VERSION_NAME)); + feedbackIntent.putExtra(Intent.EXTRA_STREAM,appLogsFilePath); + + try { + startActivity(feedbackIntent); + } catch (ActivityNotFoundException e) { + Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show(); + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java index af4f0eff4..8ef8e84a4 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java @@ -3,18 +3,17 @@ package fr.free.nrw.commons.theme; import android.content.Intent; import android.os.Bundle; import android.preference.PreferenceManager; -import android.support.v7.app.AppCompatActivity; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity; -public class BaseActivity extends AppCompatActivity { +public abstract class BaseActivity extends CommonsDaggerAppCompatActivity { boolean currentTheme; - @Override protected void onCreate(Bundle savedInstanceState) { - if(Utils.isDarkTheme(this)){ + boolean currentThemeIsDark = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false); + if (currentThemeIsDark){ currentTheme = true; setTheme(R.style.DarkAppTheme); } else { @@ -27,8 +26,8 @@ public class BaseActivity extends AppCompatActivity { @Override protected void onResume() { // Restart activity if theme is changed - boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false); - if(currentTheme!=newTheme){ //is activity theme changed + boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme", false); + if (currentTheme != newTheme) { //is activity theme changed Intent intent = getIntent(); finish(); startActivity(intent); diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index 3537e4a1d..99c9f253b 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -1,6 +1,9 @@ package fr.free.nrw.commons.theme; +import android.accounts.Account; +import android.accounts.AccountManager; import android.content.ActivityNotFoundException; +import android.content.Context; import android.content.Intent; import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; @@ -9,7 +12,9 @@ import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; import android.view.MenuItem; +import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import android.widget.Toast; import butterknife.BindView; @@ -18,9 +23,11 @@ import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.WelcomeActivity; +import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.nearby.NearbyActivity; +import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.settings.SettingsActivity; import timber.log.Timber; @@ -47,6 +54,22 @@ public abstract class NavigationBaseActivity extends BaseActivity toggle.setDrawerIndicatorEnabled(true); toggle.syncState(); setDrawerPaneWidth(); + setUserName(); + } + + /** + * Set the username in navigationHeader. + */ + private void setUserName() { + + View navHeaderView = navigationView.getHeaderView(0); + TextView username = navHeaderView.findViewById(R.id.username); + + AccountManager accountManager = AccountManager.get(this); + Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.ACCOUNT_TYPE); + if (allAccounts.length != 0) { + username.setText(allAccounts[0].name); + } } public void initBackButton() { @@ -70,30 +93,25 @@ public abstract class NavigationBaseActivity extends BaseActivity @Override public boolean onNavigationItemSelected(@NonNull final MenuItem item) { - switch (item.getItemId()) { + final int itemId = item.getItemId(); + switch (itemId) { case R.id.action_home: drawerLayout.closeDrawer(navigationView); - if (!(this instanceof ContributionsActivity)) { - ContributionsActivity.startYourself(this); - } + startActivityWithFlags( + this, ContributionsActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, + Intent.FLAG_ACTIVITY_SINGLE_TOP); return true; case R.id.action_nearby: drawerLayout.closeDrawer(navigationView); - if (!(this instanceof NearbyActivity)) { - NearbyActivity.startYourself(this); - } + startActivityWithFlags(this, NearbyActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); return true; case R.id.action_about: drawerLayout.closeDrawer(navigationView); - if (!(this instanceof AboutActivity)) { - AboutActivity.startYourself(this); - } + startActivityWithFlags(this, AboutActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); return true; case R.id.action_settings: drawerLayout.closeDrawer(navigationView); - if (!(this instanceof SettingsActivity)) { - SettingsActivity.startYourself(this); - } + startActivityWithFlags(this, SettingsActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); return true; case R.id.action_introduction: drawerLayout.closeDrawer(navigationView); @@ -126,7 +144,12 @@ public abstract class NavigationBaseActivity extends BaseActivity .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) .show(); return true; + case R.id.action_notifications: + drawerLayout.closeDrawer(navigationView); + NotificationActivity.startYourself(this); + return true; default: + Timber.e("Unknown option [%s] selected from the navigation menu", itemId); return false; } } @@ -143,4 +166,12 @@ public abstract class NavigationBaseActivity extends BaseActivity finish(); } } + + public static void startActivityWithFlags(Context context, Class cls, int... flags) { + Intent intent = new Intent(context, cls); + for (int flag: flags) { + intent.addFlags(flag); + } + context.startActivity(intent); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/CompatTextView.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/CompatTextView.java index 2508877ed..abe2e2554 100644 --- a/app/src/main/java/fr/free/nrw/commons/ui/widget/CompatTextView.java +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/CompatTextView.java @@ -1,73 +1,100 @@ -package fr.free.nrw.commons.ui.widget; - -/** - * Created by mikel on 07/08/2017. - */ - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.support.annotation.Nullable; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.AppCompatDrawableManager; -import android.support.v7.widget.AppCompatTextView; -import android.util.AttributeSet; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.utils.UiUtils; - -public class CompatTextView extends AppCompatTextView { - public CompatTextView(Context context) { - super(context); - init(null); - } - - public CompatTextView(Context context, AttributeSet attrs) { - super(context, attrs); - init(attrs); - } - - public CompatTextView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(attrs); - } - - private void init(@Nullable AttributeSet attrs) { - if (attrs != null) { - Context context = getContext(); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CompatTextView); - - // Obtain DrawableManager used to pull Drawables safely, and check if we're in RTL - AppCompatDrawableManager dm = AppCompatDrawableManager.get(); - boolean rtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL; - - // Grab the compat drawable padding from the XML - float drawablePadding = a.getDimension(R.styleable.CompatTextView_drawablePadding, 0); - - // Grab the compat drawable resources from the XML - int startDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableStart, 0); - int topDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableTop, 0); - int endDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableEnd, 0); - int bottomDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableBottom, 0); - - // Load the used drawables, fall back to whatever was set in an "android:" - Drawable[] currentDrawables = getCompoundDrawables(); - Drawable left = startDrawableRes != 0 - ? dm.getDrawable(context, startDrawableRes) : currentDrawables[0]; - Drawable right = endDrawableRes != 0 - ? dm.getDrawable(context, endDrawableRes) : currentDrawables[1]; - Drawable top = topDrawableRes != 0 - ? dm.getDrawable(context, topDrawableRes) : currentDrawables[2]; - Drawable bottom = bottomDrawableRes != 0 - ? dm.getDrawable(context, bottomDrawableRes) : currentDrawables[3]; - - // Account for RTL and apply the compound Drawables - Drawable start = rtl ? right : left; - Drawable end = rtl ? left : right; - setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); - setCompoundDrawablePadding((int) UiUtils.convertDpToPixel(drawablePadding, getContext())); - - a.recycle(); - } - } -} +package fr.free.nrw.commons.ui.widget; + +/* + *Created by mikel on 07/08/2017. + */ + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.support.annotation.Nullable; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.AppCompatDrawableManager; +import android.support.v7.widget.AppCompatTextView; +import android.util.AttributeSet; + +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.utils.UiUtils; + +/** + * a text view compatible with older versions of the platform + */ +public class CompatTextView extends AppCompatTextView { + + /** + * Constructs a new instance of CompatTextView + * + * @param context the view context + */ + public CompatTextView(Context context) { + super(context); + init(null); + } + + /** + * Constructs a new instance of CompatTextView + * + * @param context the view context + * @param attrs the set of attributes for the view + */ + public CompatTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs); + } + + /** + * Constructs a new instance of CompatTextView + * + * @param context + * @param attrs + * @param defStyleAttr + */ + public CompatTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs); + } + + /** + * initializes the view + * + * @param attrs the attribute set of the view, which can be null + */ + private void init(@Nullable AttributeSet attrs) { + if (attrs != null) { + Context context = getContext(); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CompatTextView); + + // Obtain DrawableManager used to pull Drawables safely, and check if we're in RTL + AppCompatDrawableManager dm = AppCompatDrawableManager.get(); + boolean rtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL; + + // Grab the compat drawable padding from the XML + float drawablePadding = a.getDimension(R.styleable.CompatTextView_drawablePadding, 0); + + // Grab the compat drawable resources from the XML + int startDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableStart, 0); + int topDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableTop, 0); + int endDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableEnd, 0); + int bottomDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableBottom, 0); + + // Load the used drawables, fall back to whatever was set in an "android:" + Drawable[] currentDrawables = getCompoundDrawables(); + Drawable left = startDrawableRes != 0 + ? dm.getDrawable(context, startDrawableRes) : currentDrawables[0]; + Drawable right = endDrawableRes != 0 + ? dm.getDrawable(context, endDrawableRes) : currentDrawables[1]; + Drawable top = topDrawableRes != 0 + ? dm.getDrawable(context, topDrawableRes) : currentDrawables[2]; + Drawable bottom = bottomDrawableRes != 0 + ? dm.getDrawable(context, bottomDrawableRes) : currentDrawables[3]; + + // Account for RTL and apply the compound Drawables + Drawable start = rtl ? right : left; + Drawable end = rtl ? left : right; + setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); + setCompoundDrawablePadding((int) UiUtils.convertDpToPixel(drawablePadding, getContext())); + + a.recycle(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java index 5afa5cac3..4e73776c1 100644 --- a/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java @@ -1,26 +1,51 @@ -package fr.free.nrw.commons.ui.widget; - -import android.content.Context; -import android.support.v7.widget.AppCompatTextView; -import android.text.method.LinkMovementMethod; -import android.util.AttributeSet; - -import fr.free.nrw.commons.Utils; - -/** - * An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any - * links clickable. - */ -public class HtmlTextView extends AppCompatTextView { - - public HtmlTextView(Context context, AttributeSet attrs) { - super(context, attrs); - - setMovementMethod(LinkMovementMethod.getInstance()); - setText(Utils.fromHtml(getText().toString())); - } - - public void setHtmlText(String newText) { - setText(Utils.fromHtml(newText)); - } -} +package fr.free.nrw.commons.ui.widget; + +import android.content.Context; +import android.os.Build; +import android.support.v7.widget.AppCompatTextView; +import android.text.Html; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.util.AttributeSet; + +/** + * An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any + * links clickable. + */ +public class HtmlTextView extends AppCompatTextView { + + /** + * Constructs a new instance of HtmlTextView + * @param context the context of the view + * @param attrs the set of attributes for the view + */ + public HtmlTextView(Context context, AttributeSet attrs) { + super(context, attrs); + + setMovementMethod(LinkMovementMethod.getInstance()); + setText(fromHtml(getText().toString())); + } + + /** + * Sets the text to be displayed + * @param newText the text to be displayed + */ + public void setHtmlText(String newText) { + setText(fromHtml(newText)); + } + + /** + * Fix Html.fromHtml is deprecated problem + * + * @param source provided Html string + * @return returned Spanned of appropriate method according to version check + */ + private static Spanned fromHtml(String source) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY); + } else { + //noinspection deprecation + return Html.fromHtml(source); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java index 6b2913d6d..58fccf4d7 100644 --- a/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java @@ -1,46 +1,69 @@ -package fr.free.nrw.commons.ui.widget; - -import android.app.Dialog; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.view.Gravity; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; - -public abstract class OverlayDialog extends DialogFragment { - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - setDialogLayoutToFullScreen(); - super.onViewCreated(view, savedInstanceState); - } - - private void setDialogLayoutToFullScreen() { - Window window = getDialog().getWindow(); - WindowManager.LayoutParams wlp = window.getAttributes(); - window.requestFeature(Window.FEATURE_NO_TITLE); - wlp.gravity = Gravity.BOTTOM; - wlp.width = WindowManager.LayoutParams.MATCH_PARENT; - wlp.height = WindowManager.LayoutParams.MATCH_PARENT; - window.setAttributes(wlp); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Dialog dialog = super.onCreateDialog(savedInstanceState); - Window window = dialog.getWindow(); - window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - return dialog; - } -} \ No newline at end of file +package fr.free.nrw.commons.ui.widget; + +import android.app.Dialog; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.view.Gravity; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +/** + * a formatted dialog fragment + * This class is used by NearbyInfoDialog + */ +public abstract class OverlayDialog extends DialogFragment { + + /** + * creates a DialogFragment with the correct style and theme + * @param savedInstanceState bundle re-constructed from a previous saved state + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light); + } + + /** + * When the view is created, sets the dialog layout to full screen + * + * @param view the view being used + * @param savedInstanceState bundle re-constructed from a previous saved state + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + setDialogLayoutToFullScreen(); + super.onViewCreated(view, savedInstanceState); + } + + /** + * sets the dialog layout to fullscreen + */ + private void setDialogLayoutToFullScreen() { + Window window = getDialog().getWindow(); + WindowManager.LayoutParams wlp = window.getAttributes(); + window.requestFeature(Window.FEATURE_NO_TITLE); + wlp.gravity = Gravity.BOTTOM; + wlp.width = WindowManager.LayoutParams.MATCH_PARENT; + wlp.height = WindowManager.LayoutParams.MATCH_PARENT; + window.setAttributes(wlp); + } + + /** + * builds custom dialog container + * + * @param savedInstanceState the previously saved state + * @return the dialog + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + Window window = dialog.getWindow(); + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + return dialog; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java index b76150643..fee0765a4 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java @@ -7,7 +7,6 @@ import android.support.v7.app.AlertDialog; import java.io.IOException; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.mwapi.MediaWikiApi; @@ -18,6 +17,7 @@ import timber.log.Timber; * Displays a warning to the user if the file already exists on Commons */ public class ExistingFileAsync extends AsyncTask { + interface Callback { void onResult(Result result); } @@ -28,14 +28,16 @@ public class ExistingFileAsync extends AsyncTask { DUPLICATE_CANCELLED } + private final MediaWikiApi api; private final String fileSha1; private final Context context; private final Callback callback; - public ExistingFileAsync(String fileSha1, Context context, Callback callback) { + public ExistingFileAsync(String fileSha1, Context context, Callback callback, MediaWikiApi mwApi) { this.fileSha1 = fileSha1; this.context = context; this.callback = callback; + this.api = mwApi; } @Override @@ -45,7 +47,6 @@ public class ExistingFileAsync extends AsyncTask { @Override protected Boolean doInBackground(Void... voids) { - MediaWikiApi api = CommonsApplication.getInstance().getMWApi(); // https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba boolean fileExists; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index 3c0775ac5..daa2b6d09 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -34,7 +34,7 @@ public class FileUtils { * other file-based ContentProviders. * * @param context The context. - * @param uri The Uri to query. + * @param uri The Uri to query. * @author paulburke */ // Can be safely suppressed, checks for isKitKat before running isDocumentUri @@ -55,29 +55,31 @@ public class FileUtils { if ("primary".equalsIgnoreCase(type)) { return Environment.getExternalStorageDirectory() + "/" + split[1]; } - } - // DownloadsProvider - else if (isDownloadsDocument(uri)) { + } else if (isDownloadsDocument(uri)) { // DownloadsProvider final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); return getDataColumn(context, contentUri, null, null); - } - // MediaProvider - else if (isMediaDocument(uri)) { + } else if (isMediaDocument(uri)) { // MediaProvider final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; Uri contentUri = null; - if ("image".equals(type)) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - } else if ("video".equals(type)) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; - } else if ("audio".equals(type)) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + switch (type) { + case "image": + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + break; + case "video": + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + break; + case "audio": + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + break; + default: + break; } final String selection = "_id=?"; @@ -104,7 +106,7 @@ public class FileUtils { = context.getContentResolver().openFileDescriptor(uri, "r"); if (descriptor != null) { SharedPreferences sharedPref = PreferenceManager - .getDefaultSharedPreferences(CommonsApplication.getInstance()); + .getDefaultSharedPreferences(context); boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true); if (useExtStorage) { copyPath = Environment.getExternalStorageDirectory().toString() @@ -162,8 +164,9 @@ public class FileUtils { } catch (IllegalArgumentException e) { Timber.d(e); } finally { - if (cursor != null) + if (cursor != null) { cursor.close(); + } } return null; } @@ -201,7 +204,8 @@ public class FileUtils { /** * Copy content from source file to destination file. - * @param source stream copied from + * + * @param source stream copied from * @param destination stream copied to * @throws IOException thrown when failing to read source or opening destination file */ @@ -214,7 +218,8 @@ public class FileUtils { /** * Copy content from source file to destination file. - * @param source file descriptor copied from + * + * @param source file descriptor copied from * @param destination file path copied to * @throws IOException thrown when failing to read source or opening destination file */ diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java index e7326246c..b9750e350 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java @@ -8,7 +8,6 @@ import android.location.LocationListener; import android.location.LocationManager; import android.media.ExifInterface; import android.os.Bundle; -import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; @@ -16,7 +15,6 @@ import android.support.annotation.RequiresApi; import java.io.FileDescriptor; import java.io.IOException; -import fr.free.nrw.commons.CommonsApplication; import timber.log.Timber; /** @@ -26,6 +24,8 @@ import timber.log.Timber; */ public class GPSExtractor { + private final Context context; + private SharedPreferences prefs; private ExifInterface exif; private double decLatitude; private double decLongitude; @@ -38,9 +38,12 @@ public class GPSExtractor { /** * Construct from the file descriptor of the image (only for API 24 or newer). * @param fileDescriptor the file descriptor of the image + * @param context the context */ @RequiresApi(24) - public GPSExtractor(@NonNull FileDescriptor fileDescriptor) { + public GPSExtractor(@NonNull FileDescriptor fileDescriptor, Context context, SharedPreferences prefs) { + this.context = context; + this.prefs = prefs; try { exif = new ExifInterface(fileDescriptor); } catch (IOException | IllegalArgumentException e) { @@ -51,13 +54,16 @@ public class GPSExtractor { /** * Construct from the file path of the image. * @param path file path of the image + * @param context the context */ - public GPSExtractor(@NonNull String path) { + public GPSExtractor(@NonNull String path, Context context, SharedPreferences prefs) { + this.prefs = prefs; try { exif = new ExifInterface(path); } catch (IOException | IllegalArgumentException e) { Timber.w(e); } + this.context = context; } /** @@ -65,9 +71,7 @@ public class GPSExtractor { * @return true if enabled, false if disabled */ private boolean gpsPreferenceEnabled() { - SharedPreferences sharedPref - = PreferenceManager.getDefaultSharedPreferences(CommonsApplication.getInstance()); - boolean gpsPref = sharedPref.getBoolean("allowGps", false); + boolean gpsPref = prefs.getBoolean("allowGps", false); Timber.d("Gps pref set to: %b", gpsPref); return gpsPref; } @@ -76,8 +80,7 @@ public class GPSExtractor { * Registers a LocationManager to listen for current location */ protected void registerLocationManager() { - locationManager = (LocationManager) CommonsApplication.getInstance() - .getSystemService(Context.LOCATION_SERVICE); + locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); Criteria criteria = new Criteria(); String provider = locationManager.getBestProvider(criteria, true); myLocationListener = new MyLocationListener(); @@ -110,11 +113,11 @@ public class GPSExtractor { */ @Nullable public String getCoords(boolean useGPS) { - String latitude = ""; - String longitude = ""; - String latitude_ref = ""; - String longitude_ref = ""; - String decimalCoords = ""; + String latitude; + String longitude; + String latitudeRef; + String longitudeRef; + String decimalCoords; //If image has no EXIF data and user has enabled GPS setting, get user's location if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) { @@ -147,15 +150,15 @@ public class GPSExtractor { Timber.d("EXIF data has location info"); latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); - latitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); + latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE); - longitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); + longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); - if (latitude!=null && latitude_ref!=null && longitude!=null && longitude_ref!=null) { - Timber.d("Latitude: %s %s", latitude, latitude_ref); - Timber.d("Longitude: %s %s", longitude, longitude_ref); + if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) { + Timber.d("Latitude: %s %s", latitude, latitudeRef); + Timber.d("Longitude: %s %s", longitude, longitudeRef); - decimalCoords = getDecimalCoords(latitude, latitude_ref, longitude, longitude_ref); + decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef); return decimalCoords; } else { return null; @@ -221,7 +224,7 @@ public class GPSExtractor { return decimalCoords; } - private double convertToDegree(String stringDMS){ + private double convertToDegree(String stringDMS) { double result; String[] DMS = stringDMS.split(",", 3); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java index def318fee..ac0afa979 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java @@ -2,10 +2,10 @@ package fr.free.nrw.commons.upload; import android.Manifest; import android.app.ProgressDialog; -import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.database.DataSetObserver; import android.net.Uri; @@ -27,11 +27,14 @@ import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; +import javax.inject.Named; + import butterknife.ButterKnife; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.OnCategoriesSaveHandler; import fr.free.nrw.commons.contributions.Contribution; @@ -39,26 +42,36 @@ import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModifierSequence; +import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.modifications.TemplateRemoveModifier; -import fr.free.nrw.commons.mwapi.EventLog; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; -public class MultipleShareActivity - extends AuthenticatedActivity - implements MediaDetailPagerFragment.MediaDetailProvider, - AdapterView.OnItemClickListener, - FragmentManager.OnBackStackChangedListener, - MultipleUploadListFragment.OnMultipleUploadInitiatedHandler, +public class MultipleShareActivity extends AuthenticatedActivity + implements MediaDetailPagerFragment.MediaDetailProvider, + AdapterView.OnItemClickListener, + FragmentManager.OnBackStackChangedListener, + MultipleUploadListFragment.OnMultipleUploadInitiatedHandler, OnCategoriesSaveHandler { - private CommonsApplication app; + + @Inject + MediaWikiApi mwApi; + @Inject + SessionManager sessionManager; + @Inject + UploadController uploadController; + @Inject + ModifierSequenceDao modifierSequenceDao; + @Inject + @Named("default_preferences") + SharedPreferences prefs; + private ArrayList photosList = null; private MultipleUploadListFragment uploadsList; private MediaDetailPagerFragment mediaDetails; private CategorizationFragment categorizationFragment; - private UploadController uploadController; - private boolean locationPermitted = false; @Override @@ -129,7 +142,7 @@ public class MultipleShareActivity dialog.setTitle(getResources().getQuantityString(R.plurals.starting_multiple_uploads, photosList.size(), photosList.size())); dialog.show(); - for(int i = 0; i < photosList.size(); i++) { + for (int i = 0; i < photosList.size(); i++) { Contribution up = photosList.get(i); final int uploadCount = i + 1; // Goddamn Java @@ -137,11 +150,7 @@ public class MultipleShareActivity dialog.setProgress(uploadCount); if (uploadCount == photosList.size()) { dialog.dismiss(); - Toast startingToast = Toast.makeText( - CommonsApplication.getInstance(), - R.string.uploading_started, - Toast.LENGTH_LONG - ); + Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG); startingToast.show(); } }); @@ -168,33 +177,24 @@ public class MultipleShareActivity @Override public void onCategoriesSave(List categories) { if (categories.size() > 0) { - ContentProviderClient client = getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY); - for(Contribution contribution: photosList) { + for (Contribution contribution : photosList) { ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri()); categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{}))); categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized")); - categoriesSequence.setContentProviderClient(client); - categoriesSequence.save(); + modifierSequenceDao.save(categoriesSequence); } } // FIXME: Make sure that the content provider is up // This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin - ContentResolver.setSyncAutomatically(app.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default! - EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT) - .param("username", app.getCurrentAccount().name) - .param("categories-count", categories.size()) - .param("files-count", photosList.size()) - .param("source", Contribution.SOURCE_EXTERNAL) - .param("result", "queued") - .log(); + ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, true); // Enable sync by default! finish(); } @Override public boolean onOptionsItemSelected(MenuItem item) { - switch(item.getItemId()) { + switch (item.getItemId()) { case android.R.id.home: if (mediaDetails.isVisible()) { getSupportFragmentManager().popBackStack(); @@ -207,10 +207,8 @@ public class MultipleShareActivity @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - uploadController = new UploadController(); setContentView(R.layout.activity_multiple_uploads); - app = CommonsApplication.getInstance(); ButterKnife.bind(this); initDrawer(); @@ -238,7 +236,7 @@ public class MultipleShareActivity } private void showDetail(int i) { - if (mediaDetails == null ||!mediaDetails.isVisible()) { + if (mediaDetails == null || !mediaDetails.isVisible()) { mediaDetails = new MediaDetailPagerFragment(true); getSupportFragmentManager() .beginTransaction() @@ -258,14 +256,14 @@ public class MultipleShareActivity @Override protected void onAuthCookieAcquired(String authCookie) { - app.getMWApi().setAuthCookie(authCookie); + mwApi.setAuthCookie(authCookie); Intent intent = getIntent(); - if (intent.getAction().equals(Intent.ACTION_SEND_MULTIPLE)) { + if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { if (photosList == null) { photosList = new ArrayList<>(); ArrayList urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - for(int i=0; i < urisList.size(); i++) { + for (int i = 0; i < urisList.size(); i++) { Contribution up = new Contribution(); Uri uri = urisList.get(i); up.setLocalUri(uri); @@ -284,7 +282,7 @@ public class MultipleShareActivity uploadsList = (MultipleUploadListFragment) getSupportFragmentManager().findFragmentByTag("uploadsList"); if (uploadsList == null) { - uploadsList = new MultipleUploadListFragment(); + uploadsList = new MultipleUploadListFragment(); getSupportFragmentManager() .beginTransaction() .add(R.id.uploadsFragmentContainer, uploadsList, "uploadsList") @@ -302,34 +300,9 @@ public class MultipleShareActivity finish(); } - @Override - public void onBackPressed() { - super.onBackPressed(); - if (categorizationFragment != null && categorizationFragment.isVisible()) { - EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT) - .param("username", app.getCurrentAccount().name) - .param("categories-count", categorizationFragment.getCurrentSelectedCount()) - .param("files-count", photosList.size()) - .param("source", Contribution.SOURCE_EXTERNAL) - .param("result", "cancelled") - .log(); - } else { - EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT) - .param("username", app.getCurrentAccount().name) - .param("source", getIntent().getStringExtra(UploadService.EXTRA_SOURCE)) - .param("multiple", true) - .param("result", "cancelled") - .log(); - } - } - @Override public void onBackStackChanged() { - if (mediaDetails != null && mediaDetails.isVisible()) { - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } else { - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - } + getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible()); } /** @@ -353,12 +326,12 @@ public class MultipleShareActivity if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r"); if (fd != null) { - gpsExtractor = new GPSExtractor(fd.getFileDescriptor()); + gpsExtractor = new GPSExtractor(fd.getFileDescriptor(),this,prefs); } } else { String filePath = FileUtils.getPath(this,imageUri); if (filePath != null) { - gpsExtractor = new GPSExtractor(filePath); + gpsExtractor = new GPSExtractor(filePath,this,prefs); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java index 28f88e3d6..0b6e527e5 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java @@ -28,6 +28,7 @@ import android.widget.TextView; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.view.SimpleDraweeView; +import dagger.android.support.AndroidSupportInjection; import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.media.MediaDetailPagerFragment; @@ -56,6 +57,12 @@ public class MultipleUploadListFragment extends Fragment { private RelativeLayout overlay; } + @Override + public void onAttach(Context context) { + AndroidSupportInjection.inject(this); + super.onAttach(context); + } + private class PhotoDisplayAdapter extends BaseAdapter { @Override diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java b/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java index f515f2d0c..a530e79e6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.upload; +import android.content.Context; import android.net.Uri; import com.android.volley.Cache; @@ -20,7 +21,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import fr.free.nrw.commons.CommonsApplication; import timber.log.Timber; /** @@ -33,12 +33,14 @@ public class MwVolleyApi { private static RequestQueue REQUEST_QUEUE; private static final Gson GSON = new GsonBuilder().create(); - protected static Set categorySet; + private static Set categorySet; private static List categoryList; private static final String MWURL = "https://commons.wikimedia.org/"; + private final Context context; - public MwVolleyApi() { + public MwVolleyApi(Context context) { + this.context = context; categorySet = new HashSet<>(); } @@ -67,7 +69,7 @@ public class MwVolleyApi { * @param coords Coordinates to build query with * @return URL for API query */ - private String buildUrl (String coords){ + private String buildUrl(String coords) { Uri.Builder builder = Uri.parse(MWURL).buildUpon(); @@ -93,7 +95,7 @@ public class MwVolleyApi { private synchronized RequestQueue getQueue() { if (REQUEST_QUEUE == null) { - REQUEST_QUEUE = Volley.newRequestQueue(CommonsApplication.getInstance()); + REQUEST_QUEUE = Volley.newRequestQueue(context); } return REQUEST_QUEUE; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java index fa5f0d18b..a0b9102b6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java @@ -10,13 +10,13 @@ import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.ParcelFileDescriptor; -import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.design.widget.Snackbar; import android.support.graphics.drawable.VectorDrawableCompat; import android.support.v4.app.ActivityCompat; +import android.support.v4.app.FragmentManager; import android.support.v4.content.ContextCompat; import android.view.MenuItem; import android.view.View; @@ -30,22 +30,29 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.List; +import javax.inject.Inject; +import javax.inject.Named; + import butterknife.ButterKnife; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.caching.CacheController; import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.OnCategoriesSaveHandler; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModifierSequence; +import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.modifications.TemplateRemoveModifier; -import fr.free.nrw.commons.mwapi.EventLog; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED; @@ -55,10 +62,10 @@ import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE; * Activity for the title/desc screen after image is selected. Also starts processing image * GPS coordinates or user location (if enabled in Settings) for category suggestions. */ -public class ShareActivity - extends AuthenticatedActivity +public class ShareActivity + extends AuthenticatedActivity implements SingleUploadFragment.OnUploadActionInitiated, - OnCategoriesSaveHandler { + OnCategoriesSaveHandler,SimilarImageDialogFragment.onResponse { private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1; private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2; @@ -66,7 +73,19 @@ public class ShareActivity private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4; private CategorizationFragment categorizationFragment; - private CommonsApplication app; + @Inject + MediaWikiApi mwApi; + @Inject + CacheController cacheController; + @Inject + SessionManager sessionManager; + @Inject + UploadController uploadController; + @Inject + ModifierSequenceDao modifierSequenceDao; + @Inject + @Named("default_preferences") + SharedPreferences prefs; private String source; private String mimeType; @@ -75,11 +94,10 @@ public class ShareActivity private Contribution contribution; private SimpleDraweeView backgroundImageView; - private UploadController uploadController; - private boolean cacheFound; private GPSExtractor imageObj; + private GPSExtractor tempImageObj; private String decimalCoords; private boolean useNewPermissions = false; @@ -90,7 +108,7 @@ public class ShareActivity private String description; private Snackbar snackbar; private boolean duplicateCheckPassed = false; - + private boolean haveCheckedForOtherImages = false; /** * Called when user taps the submit button. */ @@ -117,7 +135,7 @@ public class ShareActivity @RequiresApi(16) private boolean needsToRequestStoragePermission() { // We need to ask storage permission when - // the file is not owned by this app, (e.g. shared from the Gallery) + // the file is not owned by this application, (e.g. shared from the Gallery) // and permission is not obtained. return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri) && (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) @@ -127,16 +145,12 @@ public class ShareActivity private void uploadBegins() { getFileMetadata(locationPermitted); - Toast startingToast = Toast.makeText( - CommonsApplication.getInstance(), - R.string.uploading_started, - Toast.LENGTH_LONG - ); + Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG); startingToast.show(); if (!cacheFound) { //Has to be called after apiCall.request() - app.getCacheData().cacheCategory(); + cacheController.cacheCategory(); Timber.d("Cache the categories found"); } @@ -162,21 +176,13 @@ public class ShareActivity categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{}))); categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized")); - categoriesSequence.setContentProviderClient(getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY)); - categoriesSequence.save(); + modifierSequenceDao.save(categoriesSequence); } // FIXME: Make sure that the content provider is up // This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin - ContentResolver.setSyncAutomatically(app.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default! + ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, true); // Enable sync by default! - EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT) - .param("username", app.getCurrentAccount().name) - .param("categories-count", categories.size()) - .param("files-count", 1) - .param("source", contribution.getSource()) - .param("result", "queued") - .log(); finish(); } @@ -188,31 +194,9 @@ public class ShareActivity } } - @Override - public void onBackPressed() { - super.onBackPressed(); - if (categorizationFragment != null && categorizationFragment.isVisible()) { - EventLog.schema(CommonsApplication.EVENT_CATEGORIZATION_ATTEMPT) - .param("username", app.getCurrentAccount().name) - .param("categories-count", categorizationFragment.getCurrentSelectedCount()) - .param("files-count", 1) - .param("source", contribution.getSource()) - .param("result", "cancelled") - .log(); - } else { - EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT) - .param("username", app.getCurrentAccount().name) - .param("source", getIntent().getStringExtra(UploadService.EXTRA_SOURCE)) - .param("multiple", true) - .param("result", "cancelled") - .log(); - } - } - @Override protected void onAuthCookieAcquired(String authCookie) { - app.getMWApi().setAuthCookie(authCookie); - + mwApi.setAuthCookie(authCookie); } @Override @@ -225,11 +209,10 @@ public class ShareActivity @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - uploadController = new UploadController(); + setContentView(R.layout.activity_share); ButterKnife.bind(this); initBack(); - app = CommonsApplication.getInstance(); backgroundImageView = (SimpleDraweeView) findViewById(R.id.backgroundImage); backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder .newInstance(getResources()) @@ -242,7 +225,7 @@ public class ShareActivity //Receive intent from ContributionController.java when user selects picture to upload Intent intent = getIntent(); - if (intent.getAction().equals(Intent.ACTION_SEND)) { + if (Intent.ACTION_SEND.equals(intent.getAction())) { mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); if (intent.hasExtra(UploadService.EXTRA_SOURCE)) { source = intent.getStringExtra(UploadService.EXTRA_SOURCE); @@ -379,7 +362,7 @@ public class ShareActivity try { InputStream inputStream = getContentResolver().openInputStream(mediaUri); Timber.d("Input stream created from %s", mediaUri.toString()); - String fileSHA1 = Utils.getSHA1(inputStream); + String fileSHA1 = getSHA1(inputStream); Timber.d("File SHA1 is: %s", fileSHA1); ExistingFileAsync fileAsyncTask = @@ -387,7 +370,7 @@ public class ShareActivity Timber.d("%s duplicate check: %s", mediaUri.toString(), result); duplicateCheckPassed = (result == DUPLICATE_PROCEED || result == NO_DUPLICATE); - }); + }, mwApi); fileAsyncTask.execute(); } catch (IOException e) { Timber.d(e, "IO Exception: "); @@ -424,9 +407,7 @@ public class ShareActivity ParcelFileDescriptor descriptor = getContentResolver().openFileDescriptor(mediaUri, "r"); if (descriptor != null) { - SharedPreferences sharedPref = PreferenceManager - .getDefaultSharedPreferences(CommonsApplication.getInstance()); - boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true); + boolean useExtStorage = prefs.getBoolean("useExternalStorage", true); if (useExtStorage) { copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + ".jpg"; @@ -467,12 +448,12 @@ public class ShareActivity = getContentResolver().openFileDescriptor(mediaUri, "r"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (descriptor != null) { - imageObj = new GPSExtractor(descriptor.getFileDescriptor()); + imageObj = new GPSExtractor(descriptor.getFileDescriptor(), this, prefs); } } else { String filePath = getPathOfMediaOrCopy(); if (filePath != null) { - imageObj = new GPSExtractor(filePath); + imageObj = new GPSExtractor(filePath, this, prefs); } } } @@ -480,13 +461,93 @@ public class ShareActivity if (imageObj != null) { // Gets image coords from exif data or user location decimalCoords = imageObj.getCoords(gpsEnabled); - useImageCoords(); + if(decimalCoords==null || !imageObj.imageCoordsExists){ +// Check if the location is from GPS or EXIF +// Find other photos taken around the same time which has gps coordinates + Timber.d("EXIF:false"); + Timber.d("EXIF call"+(imageObj==tempImageObj)); + if(!haveCheckedForOtherImages) + findOtherImages(gpsEnabled);// Do not do repeat the process + } + else { +// As the selected image has GPS data in EXIF go ahead with the same. + useImageCoords(); + } } } catch (FileNotFoundException e) { Timber.w("File not found: " + mediaUri, e); } } + private void findOtherImages(boolean gpsEnabled) { + Timber.d("filePath"+getPathOfMediaOrCopy()); + String filePath = getPathOfMediaOrCopy(); + long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created + File folder = new File(filePath.substring(0,filePath.lastIndexOf('/'))); + File[] files = folder.listFiles(); + Timber.d("folderTime Number:"+files.length); + + for(File file : files){ + if(file.lastModified()-timeOfCreation<=(120*1000) && file.lastModified()-timeOfCreation>=-(120*1000)){ + //Make sure the photos were taken within 20seconds + Timber.d("fild date:"+file.lastModified()+ " time of creation"+timeOfCreation); + tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos + ParcelFileDescriptor descriptor + = null; + try { + descriptor = getContentResolver().openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r"); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (descriptor != null) { + tempImageObj = new GPSExtractor(descriptor.getFileDescriptor(),this, prefs); + } + } else { + if (filePath != null) { + tempImageObj = new GPSExtractor(file.getAbsolutePath(), this, prefs); + } + } + + if(tempImageObj!=null){ + Timber.d("not null fild EXIF"+tempImageObj.imageCoordsExists +" coords"+tempImageObj.getCoords(gpsEnabled)); + if(tempImageObj.getCoords(gpsEnabled)!=null && tempImageObj.imageCoordsExists){ +// Current image has gps coordinates and it's not current gps locaiton + Timber.d("This fild has image coords:"+ file.getAbsolutePath()); +// Create a dialog fragment for the suggestion + FragmentManager fragmentManager = getSupportFragmentManager(); + SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment(); + Bundle args = new Bundle(); + args.putString("originalImagePath",filePath); + args.putString("possibleImagePath",file.getAbsolutePath()); + newFragment.setArguments(args); + newFragment.show(fragmentManager, "dialog"); + break; + } + + } + + } + } + haveCheckedForOtherImages = true; //Finished checking for other images + return; + } + + @Override + public void onPostiveResponse() { + imageObj = tempImageObj; + decimalCoords = imageObj.getCoords(false);// Not necessary to use gps as image already ha EXIF data + Timber.d("EXIF from tempImageObj"); + useImageCoords(); + } + + @Override + public void onNegativeResponse() { + Timber.d("EXIF from imageObj"); + useImageCoords(); + + } + /** * Initiates retrieval of image coordinates or user coordinates, and caching of coordinates. * Then initiates the calls to MediaWiki API through an instance of MwVolleyApi. @@ -494,17 +555,18 @@ public class ShareActivity public void useImageCoords() { if (decimalCoords != null) { Timber.d("Decimal coords of image: %s", decimalCoords); + Timber.d("is EXIF data present:"+imageObj.imageCoordsExists+" from findOther image:"+(imageObj==tempImageObj)); // Only set cache for this point if image has coords if (imageObj.imageCoordsExists) { double decLongitude = imageObj.getDecLongitude(); double decLatitude = imageObj.getDecLatitude(); - app.getCacheData().setQtPoint(decLongitude, decLatitude); + cacheController.setQtPoint(decLongitude, decLatitude); } - MwVolleyApi apiCall = new MwVolleyApi(); + MwVolleyApi apiCall = new MwVolleyApi(this); - List displayCatList = app.getCacheData().findCategory(); + List displayCatList = cacheController.findCategory(); boolean catListEmpty = displayCatList.isEmpty(); // If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories @@ -517,7 +579,10 @@ public class ShareActivity Timber.d("Cache found, setting categoryList in MwVolleyApi to %s", displayCatList); MwVolleyApi.setGpsCat(displayCatList); } + }else{ + Timber.d("EXIF: no coords"); } + } @Override @@ -550,4 +615,41 @@ public class ShareActivity } return super.onOptionsItemSelected(item); } + + // Get SHA1 of file from input stream + private String getSHA1(InputStream is) { + + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA1"); + } catch (NoSuchAlgorithmException e) { + Timber.e(e, "Exception while getting Digest"); + return ""; + } + + byte[] buffer = new byte[8192]; + int read; + try { + while ((read = is.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + byte[] md5sum = digest.digest(); + BigInteger bigInt = new BigInteger(1, md5sum); + String output = bigInt.toString(16); + // Fill to 40 chars + output = String.format("%40s", output).replace(' ', '0'); + Timber.i("File SHA1: %s", output); + + return output; + } catch (IOException e) { + Timber.e(e, "IO Exception"); + return ""; + } finally { + try { + is.close(); + } catch (IOException e) { + Timber.e(e, "Exception on closing MD5 input stream"); + } + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java new file mode 100644 index 000000000..a8f336927 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java @@ -0,0 +1,112 @@ +package fr.free.nrw.commons.upload; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.graphics.drawable.VectorDrawableCompat; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.Button; + +import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; +import com.facebook.drawee.view.SimpleDraweeView; +import com.facebook.imagepipeline.listener.RequestListener; +import com.facebook.imagepipeline.listener.RequestLoggingListener; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import fr.free.nrw.commons.R; + +/** + * Created by harisanker on 14/2/18. + */ + +public class SimilarImageDialogFragment extends DialogFragment { + SimpleDraweeView originalImage; + SimpleDraweeView possibleImage; + Button positiveButton; + Button negativeButton; + onResponse mOnResponse;//Implemented interface from shareActivity + Boolean gotResponse = false; + public SimilarImageDialogFragment() { + } + public interface onResponse{ + public void onPostiveResponse(); + public void onNegativeResponse(); + } + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false); + Set requestListeners = new HashSet<>(); + requestListeners.add(new RequestLoggingListener()); + + originalImage =(SimpleDraweeView) view.findViewById(R.id.orginalImage); + possibleImage =(SimpleDraweeView) view.findViewById(R.id.possibleImage); + positiveButton = (Button) view.findViewById(R.id.postive_button); + negativeButton = (Button) view.findViewById(R.id.negative_button); + + originalImage.setHierarchy(GenericDraweeHierarchyBuilder + .newInstance(getResources()) + .setPlaceholderImage(VectorDrawableCompat.create(getResources(), + R.drawable.ic_image_black_24dp,getContext().getTheme())) + .setFailureImage(VectorDrawableCompat.create(getResources(), + R.drawable.ic_error_outline_black_24dp, getContext().getTheme())) + .build()); + possibleImage.setHierarchy(GenericDraweeHierarchyBuilder + .newInstance(getResources()) + .setPlaceholderImage(VectorDrawableCompat.create(getResources(), + R.drawable.ic_image_black_24dp,getContext().getTheme())) + .setFailureImage(VectorDrawableCompat.create(getResources(), + R.drawable.ic_error_outline_black_24dp, getContext().getTheme())) + .build()); + + originalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath")))); + possibleImage.setImageURI(Uri.fromFile(new File(getArguments().getString("possibleImagePath")))); + + negativeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mOnResponse.onNegativeResponse(); + gotResponse = true; + dismiss(); + } + }); + positiveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mOnResponse.onPostiveResponse(); + gotResponse = true; + dismiss(); + } + }); + return view; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mOnResponse = (onResponse) getActivity();//Interface Implementation + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + return dialog; + } + + @Override + public void onDismiss(DialogInterface dialog) { +// I user dismisses dialog by pressing outside the dialog. + if(!gotResponse) + mOnResponse.onNegativeResponse(); + super.onDismiss(dialog); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java index 579352991..099790495 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.upload; +import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -7,10 +8,11 @@ import android.graphics.Color; import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; -import android.support.v4.app.Fragment; +import android.support.annotation.NonNull; import android.support.v7.app.AlertDialog; import android.text.Editable; import android.text.TextWatcher; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -28,6 +30,9 @@ import android.widget.TextView; import java.util.ArrayList; +import javax.inject.Inject; +import javax.inject.Named; + import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; @@ -35,13 +40,14 @@ import butterknife.OnItemSelected; import butterknife.OnTouch; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.settings.Prefs; import timber.log.Timber; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; -public class SingleUploadFragment extends Fragment { +public class SingleUploadFragment extends CommonsDaggerSupportFragment { @BindView(R.id.titleEdit) EditText titleEdit; @BindView(R.id.descEdit) EditText descEdit; @@ -49,7 +55,8 @@ public class SingleUploadFragment extends Fragment { @BindView(R.id.share_license_summary) TextView licenseSummaryView; @BindView(R.id.licenseSpinner) Spinner licenseSpinner; - private SharedPreferences prefs; + @Inject @Named("default_preferences") SharedPreferences prefs; + private String license; private OnUploadActionInitiated uploadActionInitiatedHandler; private TitleTextWatcher textWatcher = new TitleTextWatcher(); @@ -72,11 +79,10 @@ public class SingleUploadFragment extends Fragment { String desc = descEdit.getText().toString(); //Save the title/desc in short-lived cache so next time this fragment is loaded, we can access these - SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity()); - SharedPreferences.Editor editor = titleDesc.edit(); - editor.putString("Title", title); - editor.putString("Desc", desc); - editor.apply(); + prefs.edit() + .putString("Title", title) + .putString("Desc", desc) + .apply(); uploadActionInitiatedHandler.uploadActionInitiated(title, desc); return true; @@ -91,7 +97,6 @@ public class SingleUploadFragment extends Fragment { View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false); ButterKnife.bind(this, rootView); - ArrayList licenseItems = new ArrayList<>(); licenseItems.add(getString(R.string.license_name_cc0)); licenseItems.add(getString(R.string.license_name_cc_by)); @@ -99,7 +104,6 @@ public class SingleUploadFragment extends Fragment { licenseItems.add(getString(R.string.license_name_cc_by_four)); licenseItems.add(getString(R.string.license_name_cc_by_sa_four)); - prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); // check if this is the first time we have uploaded @@ -134,11 +138,29 @@ public class SingleUploadFragment extends Fragment { titleEdit.addTextChangedListener(textWatcher); + titleEdit.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + hideKeyboard(v); + } + }); + + descEdit.setOnFocusChangeListener((v, hasFocus) -> { + if(!hasFocus){ + hideKeyboard(v); + } + }); + setLicenseSummary(license); return rootView; } + public void hideKeyboard(View view) { + Log.i("hide", "hideKeyboard: "); + InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + @Override public void onDestroyView() { titleEdit.removeTextChangedListener(textWatcher); @@ -172,9 +194,9 @@ public class SingleUploadFragment extends Fragment { } setLicenseSummary(license); - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(Prefs.DEFAULT_LICENSE, license); - editor.commit(); + prefs.edit() + .putString(Prefs.DEFAULT_LICENSE, license) + .commit(); } @OnTouch(R.id.share_license_summary) @@ -182,7 +204,7 @@ public class SingleUploadFragment extends Fragment { if (motionEvent.getActionMasked() == ACTION_DOWN) { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(Utils.licenseUrlFor(license))); + intent.setData(Uri.parse(licenseUrlFor(license))); startActivity(intent); return true; } else { @@ -193,9 +215,8 @@ public class SingleUploadFragment extends Fragment { @OnClick(R.id.titleDescButton) void setTitleDescButton() { //Retrieve last title and desc entered - SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity()); - String title = titleDesc.getString("Title", ""); - String desc = titleDesc.getString("Desc", ""); + String title = prefs.getString("Title", ""); + String desc = prefs.getString("Desc", ""); Timber.d("Title: %s, Desc: %s", title, desc); titleEdit.setText(title); @@ -263,6 +284,23 @@ public class SingleUploadFragment extends Fragment { } } + @NonNull + private String licenseUrlFor(String license) { + switch (license) { + case Prefs.Licenses.CC_BY_3: + return "https://creativecommons.org/licenses/by/3.0/"; + case Prefs.Licenses.CC_BY_4: + return "https://creativecommons.org/licenses/by/4.0/"; + case Prefs.Licenses.CC_BY_SA_3: + return "https://creativecommons.org/licenses/by-sa/3.0/"; + case Prefs.Licenses.CC_BY_SA_4: + return "https://creativecommons.org/licenses/by-sa/4.0/"; + case Prefs.Licenses.CC0: + return "https://creativecommons.org/publicdomain/zero/1.0/"; + } + throw new RuntimeException("Unrecognized license value: " + license); + } + public interface OnUploadActionInitiated { void uploadActionInitiated(String title, String description); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index 4a41fc4d1..32554da0f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -1,74 +1,103 @@ package fr.free.nrw.commons.upload; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; +import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.IBinder; -import android.preference.PreferenceManager; import android.provider.MediaStore; import android.text.TextUtils; +import java.io.BufferedInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Date; import java.util.concurrent.Executors; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.HandlerService; -import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.settings.Prefs; import timber.log.Timber; public class UploadController { private UploadService uploadService; - private final CommonsApplication app; + private SessionManager sessionManager; + private Context context; + private SharedPreferences prefs; public interface ContributionUploadProgress { void onUploadStarted(Contribution contribution); } - public UploadController() { - app = CommonsApplication.getInstance(); + /** + * Constructs a new UploadController. + */ + public UploadController(SessionManager sessionManager, Context context, SharedPreferences sharedPreferences) { + this.sessionManager = sessionManager; + this.context = context; + this.prefs = sharedPreferences; } private boolean isUploadServiceConnected; private ServiceConnection uploadServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder binder) { - uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder)binder).getService(); + uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder).getService(); isUploadServiceConnected = true; } @Override public void onServiceDisconnected(ComponentName componentName) { // this should never happen - throw new RuntimeException("UploadService died but the rest of the process did not!"); + Timber.e(new RuntimeException("UploadService died but the rest of the process did not!")); } }; + /** + * Prepares the upload service. + */ public void prepareService() { - Intent uploadServiceIntent = new Intent(app, UploadService.class); + Intent uploadServiceIntent = new Intent(context, UploadService.class); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); - app.startService(uploadServiceIntent); - app.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); + context.startService(uploadServiceIntent); + context.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); } + /** + * Disconnects the upload service. + */ public void cleanup() { - if(isUploadServiceConnected) { - app.unbindService(uploadServiceConnection); + if (isUploadServiceConnected) { + context.unbindService(uploadServiceConnection); } } + /** + * Starts a new upload task. + * + * @param title the title of the contribution + * @param mediaUri the media URI of the contribution + * @param description the description of the contribution + * @param mimeType the MIME type of the contribution + * @param source the source of the contribution + * @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615") + * @param onComplete the progress tracker + */ public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) { Contribution contribution; //TODO: Modify this to include coords - contribution = new Contribution(mediaUri, null, title, description, -1, null, null, app.getCurrentAccount().name, CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords); + contribution = new Contribution(mediaUri, null, title, description, -1, + null, null, sessionManager.getCurrentAccount().name, + CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords); contribution.setTag("mimeType", mimeType); contribution.setSource(source); @@ -77,16 +106,19 @@ public class UploadController { startUpload(contribution, onComplete); } + /** + * Starts a new upload task. + * + * @param contribution the contribution object + * @param onComplete the progress tracker + */ public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) { - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); - //Set creator, desc, and license - if(TextUtils.isEmpty(contribution.getCreator())) { - contribution.setCreator(app.getCurrentAccount().name); + if (TextUtils.isEmpty(contribution.getCreator())) { + contribution.setCreator(sessionManager.getCurrentAccount().name); } - if(contribution.getDescription() == null) { + if (contribution.getDescription() == null) { contribution.setDescription(""); } @@ -102,17 +134,20 @@ public class UploadController { @Override protected Contribution doInBackground(Void... voids /* stare into you */) { long length; + ContentResolver contentResolver = context.getContentResolver(); try { - if(contribution.getDataLength() <= 0) { - length = app.getContentResolver() - .openAssetFileDescriptor(contribution.getLocalUri(), "r") - .getLength(); - if(length == -1) { - // Let us find out the long way! - length = Utils.countBytes(app.getContentResolver() - .openInputStream(contribution.getLocalUri())); + if (contribution.getDataLength() <= 0) { + AssetFileDescriptor assetFileDescriptor = contentResolver + .openAssetFileDescriptor(contribution.getLocalUri(), "r"); + if (assetFileDescriptor != null) { + length = assetFileDescriptor.getLength(); + if (length == -1) { + // Let us find out the long way! + length = countBytes(contentResolver + .openInputStream(contribution.getLocalUri())); + } + contribution.setDataLength(length); } - contribution.setDataLength(length); } } catch (IOException e) { Timber.e(e, "IO Exception: "); @@ -122,11 +157,11 @@ public class UploadController { Timber.e(e, "Security Exception: "); } - String mimeType = (String)contribution.getTag("mimeType"); + String mimeType = (String) contribution.getTag("mimeType"); Boolean imagePrefix = false; if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) { - mimeType = app.getContentResolver().getType(contribution.getLocalUri()); + mimeType = contentResolver.getType(contribution.getLocalUri()); } if (mimeType != null) { @@ -137,7 +172,7 @@ public class UploadController { if (imagePrefix && contribution.getDateCreated() == null) { Timber.d("local uri " + contribution.getLocalUri()); - Cursor cursor = app.getContentResolver().query(contribution.getLocalUri(), + Cursor cursor = contentResolver.query(contribution.getLocalUri(), new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null); if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) { cursor.moveToFirst(); @@ -165,4 +200,21 @@ public class UploadController { } }.executeOnExecutor(Executors.newFixedThreadPool(1)); // TODO remove this by using a sensible thread handling strategy } + + + /** + * Counts the number of bytes in {@code stream}. + * + * @param stream the stream + * @return the number of bytes in {@code stream} + * @throws IOException if an I/O error occurs + */ + private long countBytes(InputStream stream) throws IOException { + long count = 0; + BufferedInputStream bis = new BufferedInputStream(stream); + while (bis.read() != -1) { + count++; + } + return count; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index f6e4ee6a2..94c005256 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -4,10 +4,10 @@ import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Intent; +import android.content.SharedPreferences; import android.graphics.BitmapFactory; import android.os.Bundle; import android.support.v4.app.NotificationCompat; @@ -22,15 +22,18 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import fr.free.nrw.commons.CommonsApplication; +import javax.inject.Inject; +import javax.inject.Named; + import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; +import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider; -import fr.free.nrw.commons.mwapi.EventLog; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.UploadResult; import timber.log.Timber; @@ -45,12 +48,13 @@ public class UploadService extends HandlerService { public static final String EXTRA_SOURCE = EXTRA_PREFIX + ".source"; public static final String EXTRA_CAMPAIGN = EXTRA_PREFIX + ".campaign"; + @Inject MediaWikiApi mwApi; + @Inject SessionManager sessionManager; + @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject ContributionDao contributionDao; + private NotificationManager notificationManager; - private ContentProviderClient contributionsProviderClient; - private CommonsApplication app; - private NotificationCompat.Builder curProgressNotification; - private int toUpload; // The file names of unfinished uploads, used to prevent overwriting @@ -101,7 +105,7 @@ public class UploadService extends HandlerService { startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build()); contribution.setTransferred(transferred); - contribution.save(); + contributionDao.save(contribution); } } @@ -109,7 +113,6 @@ public class UploadService extends HandlerService { @Override public void onDestroy() { super.onDestroy(); - contributionsProviderClient.release(); Timber.d("UploadService.onDestroy; %s are yet to be uploaded", unfinishedUploads); } @@ -118,8 +121,6 @@ public class UploadService extends HandlerService { super.onCreate(); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - app = CommonsApplication.getInstance(); - contributionsProviderClient = this.getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY); } @Override @@ -141,9 +142,7 @@ public class UploadService extends HandlerService { contribution.setState(Contribution.STATE_QUEUED); contribution.setTransferred(0); - contribution.setContentProviderClient(contributionsProviderClient); - - contribution.save(); + contributionDao.save(contribution); toUpload++; if (curProgressNotification != null && toUpload != 1) { curProgressNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload)); @@ -162,13 +161,13 @@ public class UploadService extends HandlerService { @Override public int onStartCommand(Intent intent, int flags, int startId) { - if (intent.getAction().equals(ACTION_START_SERVICE) && freshStart) { + if (ACTION_START_SERVICE.equals(intent.getAction()) && freshStart) { ContentValues failedValues = new ContentValues(); - failedValues.put(Contribution.Table.COLUMN_STATE, Contribution.STATE_FAILED); + failedValues.put(ContributionDao.Table.COLUMN_STATE, Contribution.STATE_FAILED); int updated = getContentResolver().update(ContributionsContentProvider.BASE_URI, failedValues, - Contribution.Table.COLUMN_STATE + " = ? OR " + Contribution.Table.COLUMN_STATE + " = ?", + ContributionDao.Table.COLUMN_STATE + " = ? OR " + ContributionDao.Table.COLUMN_STATE + " = ?", new String[]{ String.valueOf(Contribution.STATE_QUEUED), String.valueOf(Contribution.STATE_IN_PROGRESS) } ); Timber.d("Set %d uploads to failed", updated); @@ -180,9 +179,7 @@ public class UploadService extends HandlerService { @SuppressLint("StringFormatInvalid") private void uploadContribution(Contribution contribution) { - MediaWikiApi api = app.getMWApi(); - - InputStream file = null; + InputStream file; String notificationTag = contribution.getLocalUri().toString(); @@ -193,6 +190,14 @@ public class UploadService extends HandlerService { Timber.d("File not found"); Toast fileNotFound = Toast.makeText(this, R.string.upload_failed, Toast.LENGTH_LONG); fileNotFound.show(); + return; + } + + //As the file is null there's no point in continuing the upload process + //mwapi.upload accepts a NonNull input stream + if(file == null) { + Timber.d("File not found"); + return; } Timber.d("Before execution!"); @@ -220,9 +225,9 @@ public class UploadService extends HandlerService { filename = findUniqueFilename(filename); unfinishedUploads.add(filename); } - if (!api.validateLogin()) { + if (!mwApi.validateLogin()) { // Need to revalidate! - if (app.revalidateAuthToken()) { + if (sessionManager.revalidateAuthToken()) { Timber.d("Successfully revalidated token!"); } else { Timber.d("Unable to revalidate :("); @@ -238,7 +243,7 @@ public class UploadService extends HandlerService { getString(R.string.upload_progress_notification_title_finishing, contribution.getDisplayTitle()), contribution ); - UploadResult uploadResult = api.uploadFile(filename, file, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater); + UploadResult uploadResult = mwApi.uploadFile(filename, file, contribution.getDataLength(), contribution.getPageContents(), contribution.getEditSummary(), notificationUpdater); Timber.d("Response is %s", uploadResult.toString()); @@ -247,27 +252,12 @@ public class UploadService extends HandlerService { String resultStatus = uploadResult.getResultStatus(); if (!resultStatus.equals("Success")) { showFailedNotification(contribution); - EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT) - .param("username", app.getCurrentAccount().name) - .param("source", contribution.getSource()) - .param("multiple", contribution.getMultiple()) - .param("result", uploadResult.getErrorCode()) - .param("filename", contribution.getFilename()) - .log(); } else { contribution.setFilename(uploadResult.getCanonicalFilename()); contribution.setImageUrl(uploadResult.getImageUrl()); contribution.setState(Contribution.STATE_COMPLETED); contribution.setDateUploaded(uploadResult.getDateUploaded()); - contribution.save(); - - EventLog.schema(CommonsApplication.EVENT_UPLOAD_ATTEMPT) - .param("username", app.getCurrentAccount().name) - .param("source", contribution.getSource()) //FIXME - .param("filename", contribution.getFilename()) - .param("multiple", contribution.getMultiple()) - .param("result", "success") - .log(); + contributionDao.save(contribution); } } catch (IOException e) { Timber.d("I have a network fuckup"); @@ -279,7 +269,7 @@ public class UploadService extends HandlerService { toUpload--; if (toUpload == 0) { // Sync modifications right after all uplaods are processed - ContentResolver.requestSync((CommonsApplication.getInstance()).getCurrentAccount(), ModificationsContentProvider.AUTHORITY, new Bundle()); + ContentResolver.requestSync(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, new Bundle()); stopForeground(true); } } @@ -298,11 +288,10 @@ public class UploadService extends HandlerService { notificationManager.notify(NOTIFICATION_UPLOAD_FAILED, failureNotification); contribution.setState(Contribution.STATE_FAILED); - contribution.save(); + contributionDao.save(contribution); } private String findUniqueFilename(String fileName) throws IOException { - MediaWikiApi api = app.getMWApi(); String sequenceFileName; for (int sequenceNumber = 1; true; sequenceNumber++) { if (sequenceNumber == 1) { @@ -318,7 +307,7 @@ public class UploadService extends HandlerService { sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2"); } } - if (!api.fileExistsWithName(sequenceFileName) + if (!mwApi.fileExistsWithName(sequenceFileName) && !unfinishedUploads.contains(sequenceFileName)) { break; } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java new file mode 100644 index 000000000..6b3bf0377 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/DateUtils.java @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.utils; + +import java.util.Calendar; +import java.util.Date; + +public class DateUtils { + public static String getTimeAgo(Date currDate, Date itemDate) { + Calendar c = Calendar.getInstance(); + c.setTime(currDate); + int yearNow = c.get(Calendar.YEAR); + int monthNow = c.get(Calendar.MONTH); + int dayNow = c.get(Calendar.DAY_OF_MONTH); + int hourNow = c.get(Calendar.HOUR_OF_DAY); + int minuteNow = c.get(Calendar.MINUTE); + c.setTime(itemDate); + int videoYear = c.get(Calendar.YEAR); + int videoMonth = c.get(Calendar.MONTH); + int videoDays = c.get(Calendar.DAY_OF_MONTH); + int videoHour = c.get(Calendar.HOUR_OF_DAY); + int videoMinute = c.get(Calendar.MINUTE); + + if (yearNow != videoYear) { + return (String.valueOf(yearNow - videoYear) + "-" + "years"); + } else if (monthNow != videoMonth) { + return (String.valueOf(monthNow - videoMonth) + "-" + "months"); + } else if (dayNow != videoDays) { + return (String.valueOf(dayNow - videoDays) + "-" + "days"); + } else if (hourNow != videoHour) { + return (String.valueOf(hourNow - videoHour) + "-" + "hours"); + } else if (minuteNow != videoMinute) { + return (String.valueOf(minuteNow - videoMinute) + "-" + "minutes"); + } else { + return "0-seconds"; + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java index 3992324b5..78c1ca155 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/DialogUtil.java @@ -11,6 +11,11 @@ import timber.log.Timber; public class DialogUtil { + /** + * Dismisses a dialog safely. + * @param activity the activity + * @param dialog the dialog to be dismissed + */ public static void dismissSafely(@Nullable Activity activity, @Nullable DialogFragment dialog) { boolean isActivityDestroyed = false; @@ -33,6 +38,11 @@ public class DialogUtil { } } + /** + * Shows a dialog safely. + * @param activity the activity + * @param dialog the dialog to be shown + */ public static void showSafely(Activity activity, Dialog dialog) { if (activity == null || dialog == null) { Timber.d("Show called with null activity / dialog. Ignoring."); @@ -54,6 +64,11 @@ public class DialogUtil { } } + /** + * Shows a dialog safely. + * @param activity the activity + * @param dialog the dialog to be shown + */ public static void showSafely(FragmentActivity activity, DialogFragment dialog) { boolean isActivityDestroyed = false; diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java index df67cfea7..57ed86eeb 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java @@ -15,6 +15,8 @@ public class ExecutorUtils { } }; - public static Executor uiExecutor () { return uiExecutor;} + public static Executor uiExecutor() { + return uiExecutor; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java index 1c816b9e1..d56a7b608 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java @@ -1,29 +1,37 @@ package fr.free.nrw.commons.utils; +import android.os.Environment; + import java.io.BufferedReader; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStreamWriter; -import fr.free.nrw.commons.CommonsApplication; +import timber.log.Timber; public class FileUtils { /** * Read and return the content of a resource file as string. * - * @param fileName asset file's path (e.g. "/assets/queries/nearby_query.rq") + * @param fileName asset file's path (e.g. "/queries/nearby_query.rq") * @return the content of the file */ public static String readFromResource(String fileName) throws IOException { StringBuilder buffer = new StringBuilder(); BufferedReader reader = null; try { - reader = new BufferedReader( - new InputStreamReader( - CommonsApplication.class.getResourceAsStream(fileName), "UTF-8")); + InputStream inputStream = FileUtils.class.getResourceAsStream(fileName); + if (inputStream == null) { + throw new FileNotFoundException(fileName); + } + reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); String line; while ((line = reader.readLine()) != null) { - buffer.append(line + "\n"); + buffer.append(line).append("\n"); } } finally { if (reader != null) { @@ -53,5 +61,32 @@ public class FileUtils { return deletedAll; } + public static File createAndGetAppLogsFile(String logs) { + try { + File commonsAppDirectory = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); + if (!commonsAppDirectory.exists()) { + commonsAppDirectory.mkdir(); + } + File logsFile = new File(commonsAppDirectory,"logs.txt"); + if (logsFile.exists()) { + //old logs file is useless + logsFile.delete(); + } + + logsFile.createNewFile(); + + FileOutputStream outputStream = new FileOutputStream(logsFile); + OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); + outputStreamWriter.append(logs); + outputStreamWriter.close(); + outputStream.flush(); + outputStream.close(); + + return logsFile; + } catch (IOException ioe) { + Timber.e(ioe); + return null; + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java index aa79513e8..d0e432c33 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java @@ -23,8 +23,8 @@ public class FragmentUtils { .commitNow(); return true; } catch (IllegalStateException e) { - Timber.e(e, "Could not add & commit fragment. " + - "Did you mean to call commitAllowingStateLoss?"); + Timber.e(e, "Could not add & commit fragment. " + + "Did you mean to call commitAllowingStateLoss?"); } return false; } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java index 6fd9f9612..8b687164b 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java @@ -37,6 +37,13 @@ public class LengthUtils { return computeAngleBetween(from, to) * 6371009.0D; // Earth's radius in meter } + /** + * Computes angle between two points + * + * @param from Point A + * @param to Point B + * @return Angle in radius + */ private static double computeAngleBetween(LatLng from, LatLng to) { return distanceRadians(Math.toRadians(from.getLatitude()), Math.toRadians(from.getLongitude()), @@ -44,18 +51,43 @@ public class LengthUtils { Math.toRadians(to.getLongitude())); } + /** + * Computes arc length between 2 points + * @param lat1 Latitude of point A + * @param lng1 Longitude of point A + * @param lat2 Latitude of point B + * @param lng2 Longitude of point B + * @return Arc length between the points + */ private static double distanceRadians(double lat1, double lng1, double lat2, double lng2) { return arcHav(havDistance(lat1, lat2, lng1 - lng2)); } + /** + * Computes inverse of haversine + * @param x Angle in radian + * @return Inverse of haversine + */ private static double arcHav(double x) { return 2.0D * Math.asin(Math.sqrt(x)); } + /** + * Computes distance between two points that are on same Longitude + * @param lat1 Latitude of point A + * @param lat2 Latitude of point B + * @param longitude Longitude on which they lie + * @return Arc length between points + */ private static double havDistance(double lat1, double lat2, double longitude) { return hav(lat1 - lat2) + hav(longitude) * Math.cos(lat1) * Math.cos(lat2); } + /** + * Computes haversine + * @param x Angle in radians + * @return Haversine of x + */ private static double hav(double x) { double sinHalf = Math.sin(x * 0.5D); return sinHalf * sinHalf; diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java new file mode 100644 index 000000000..f2a02398f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.utils; + +import android.content.Context; +import android.support.annotation.StringRes; +import android.widget.Toast; + +public class ViewUtil { + + public static void showLongToast(final Context context, @StringRes final int stringResId) { + ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResId), Toast.LENGTH_LONG).show()); + } +} diff --git a/app/src/main/res/drawable-hdpi/welcome_copyright.png b/app/src/main/res/drawable-hdpi/welcome_copyright.png deleted file mode 100644 index 48f90bfde..000000000 Binary files a/app/src/main/res/drawable-hdpi/welcome_copyright.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/welcome_copyright.webp b/app/src/main/res/drawable-hdpi/welcome_copyright.webp new file mode 100644 index 000000000..34b5327cf Binary files /dev/null and b/app/src/main/res/drawable-hdpi/welcome_copyright.webp differ diff --git a/app/src/main/res/drawable-hdpi/welcome_wikipedia.png b/app/src/main/res/drawable-hdpi/welcome_wikipedia.png deleted file mode 100644 index 6e1a8ae8c..000000000 Binary files a/app/src/main/res/drawable-hdpi/welcome_wikipedia.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/welcome_wikipedia.webp b/app/src/main/res/drawable-hdpi/welcome_wikipedia.webp new file mode 100644 index 000000000..96ce4e276 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/welcome_wikipedia.webp differ diff --git a/app/src/main/res/drawable-ldpi/welcome_copyright.png b/app/src/main/res/drawable-ldpi/welcome_copyright.png deleted file mode 100644 index 6cdf2520e..000000000 Binary files a/app/src/main/res/drawable-ldpi/welcome_copyright.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/welcome_copyright.webp b/app/src/main/res/drawable-ldpi/welcome_copyright.webp new file mode 100644 index 000000000..c1bd8a976 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/welcome_copyright.webp differ diff --git a/app/src/main/res/drawable-ldpi/welcome_wikipedia.png b/app/src/main/res/drawable-ldpi/welcome_wikipedia.png deleted file mode 100644 index e55b56421..000000000 Binary files a/app/src/main/res/drawable-ldpi/welcome_wikipedia.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/welcome_wikipedia.webp b/app/src/main/res/drawable-ldpi/welcome_wikipedia.webp new file mode 100644 index 000000000..dd8090b75 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/welcome_wikipedia.webp differ diff --git a/app/src/main/res/drawable-mdpi/welcome_copyright.png b/app/src/main/res/drawable-mdpi/welcome_copyright.png deleted file mode 100644 index 65462b967..000000000 Binary files a/app/src/main/res/drawable-mdpi/welcome_copyright.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/welcome_copyright.webp b/app/src/main/res/drawable-mdpi/welcome_copyright.webp new file mode 100644 index 000000000..69e238747 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/welcome_copyright.webp differ diff --git a/app/src/main/res/drawable-mdpi/welcome_wikipedia.png b/app/src/main/res/drawable-mdpi/welcome_wikipedia.png deleted file mode 100644 index 8d1e8e322..000000000 Binary files a/app/src/main/res/drawable-mdpi/welcome_wikipedia.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/welcome_wikipedia.webp b/app/src/main/res/drawable-mdpi/welcome_wikipedia.webp new file mode 100644 index 000000000..5a50f5757 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/welcome_wikipedia.webp differ diff --git a/app/src/main/res/drawable-xhdpi/welcome_copyright.png b/app/src/main/res/drawable-xhdpi/welcome_copyright.png deleted file mode 100644 index 973fb4598..000000000 Binary files a/app/src/main/res/drawable-xhdpi/welcome_copyright.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/welcome_copyright.webp b/app/src/main/res/drawable-xhdpi/welcome_copyright.webp new file mode 100644 index 000000000..2feb197d2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/welcome_copyright.webp differ diff --git a/app/src/main/res/drawable-xhdpi/welcome_wikipedia.png b/app/src/main/res/drawable-xhdpi/welcome_wikipedia.png deleted file mode 100644 index 2f8dfb364..000000000 Binary files a/app/src/main/res/drawable-xhdpi/welcome_wikipedia.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/welcome_wikipedia.webp b/app/src/main/res/drawable-xhdpi/welcome_wikipedia.webp new file mode 100644 index 000000000..48e83d760 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/welcome_wikipedia.webp differ diff --git a/app/src/main/res/drawable/blue_rinse_circle.xml b/app/src/main/res/drawable/blue_rinse_circle.xml new file mode 100644 index 000000000..e63317a5b --- /dev/null +++ b/app/src/main/res/drawable/blue_rinse_circle.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/empty_photo.png b/app/src/main/res/drawable/empty_photo.png deleted file mode 100644 index 2ad718194..000000000 Binary files a/app/src/main/res/drawable/empty_photo.png and /dev/null differ diff --git a/app/src/main/res/drawable/empty_photo.webp b/app/src/main/res/drawable/empty_photo.webp new file mode 100644 index 000000000..3f749936a Binary files /dev/null and b/app/src/main/res/drawable/empty_photo.webp differ diff --git a/app/src/main/res/drawable/ic_action_facebook.xml b/app/src/main/res/drawable/ic_action_facebook.xml new file mode 100644 index 000000000..e17144640 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_facebook.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_action_github.xml b/app/src/main/res/drawable/ic_action_github.xml new file mode 100644 index 000000000..569994ebf --- /dev/null +++ b/app/src/main/res/drawable/ic_action_github.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_action_website.xml b/app/src/main/res/drawable/ic_action_website.xml new file mode 100644 index 000000000..bad3beb94 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_website.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_chat_bubble_black_24px.xml b/app/src/main/res/drawable/ic_chat_bubble_black_24px.xml new file mode 100644 index 000000000..8d40c6d63 --- /dev/null +++ b/app/src/main/res/drawable/ic_chat_bubble_black_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit_black_24dp.xml b/app/src/main/res/drawable/ic_edit_black_24dp.xml new file mode 100644 index 000000000..2ab2fb753 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_message_black_24dp.xml b/app/src/main/res/drawable/ic_message_black_24dp.xml new file mode 100644 index 000000000..d2876bfad --- /dev/null +++ b/app/src/main/res/drawable/ic_message_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml new file mode 100644 index 000000000..7009a6763 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/llamas.png b/app/src/main/res/drawable/llamas.png deleted file mode 100644 index fc3e258d5..000000000 Binary files a/app/src/main/res/drawable/llamas.png and /dev/null differ diff --git a/app/src/main/res/drawable/llamas.webp b/app/src/main/res/drawable/llamas.webp new file mode 100644 index 000000000..6e9b013d6 Binary files /dev/null and b/app/src/main/res/drawable/llamas.webp differ diff --git a/app/src/main/res/drawable/mount_zao.png b/app/src/main/res/drawable/mount_zao.png deleted file mode 100644 index 86b98661e..000000000 Binary files a/app/src/main/res/drawable/mount_zao.png and /dev/null differ diff --git a/app/src/main/res/drawable/mount_zao.webp b/app/src/main/res/drawable/mount_zao.webp new file mode 100644 index 000000000..83fd07b30 Binary files /dev/null and b/app/src/main/res/drawable/mount_zao.webp differ diff --git a/app/src/main/res/drawable/rainbow_bridge.png b/app/src/main/res/drawable/rainbow_bridge.png deleted file mode 100644 index a3f78efe6..000000000 Binary files a/app/src/main/res/drawable/rainbow_bridge.png and /dev/null differ diff --git a/app/src/main/res/drawable/rainbow_bridge.webp b/app/src/main/res/drawable/rainbow_bridge.webp new file mode 100644 index 000000000..890ebbca5 Binary files /dev/null and b/app/src/main/res/drawable/rainbow_bridge.webp differ diff --git a/app/src/main/res/drawable/selfie_x.png b/app/src/main/res/drawable/selfie_x.png deleted file mode 100644 index c049e2f18..000000000 Binary files a/app/src/main/res/drawable/selfie_x.png and /dev/null differ diff --git a/app/src/main/res/drawable/selfie_x.webp b/app/src/main/res/drawable/selfie_x.webp new file mode 100644 index 000000000..35c6c0c25 Binary files /dev/null and b/app/src/main/res/drawable/selfie_x.webp differ diff --git a/app/src/main/res/drawable/sydney_opera_house.png b/app/src/main/res/drawable/sydney_opera_house.png deleted file mode 100644 index fc1009cd1..000000000 Binary files a/app/src/main/res/drawable/sydney_opera_house.png and /dev/null differ diff --git a/app/src/main/res/drawable/sydney_opera_house.webp b/app/src/main/res/drawable/sydney_opera_house.webp new file mode 100644 index 000000000..fd7453004 Binary files /dev/null and b/app/src/main/res/drawable/sydney_opera_house.webp differ diff --git a/app/src/main/res/drawable/tulip.png b/app/src/main/res/drawable/tulip.png deleted file mode 100644 index 6467a0701..000000000 Binary files a/app/src/main/res/drawable/tulip.png and /dev/null differ diff --git a/app/src/main/res/drawable/tulip.webp b/app/src/main/res/drawable/tulip.webp new file mode 100644 index 000000000..21a8a21df Binary files /dev/null and b/app/src/main/res/drawable/tulip.webp differ diff --git a/app/src/main/res/layout-land/activity_login.xml b/app/src/main/res/layout-land/activity_login.xml new file mode 100644 index 000000000..9ecaf9855 --- /dev/null +++ b/app/src/main/res/layout-land/activity_login.xml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +