diff --git a/.gitignore b/.gitignore index 1ab05305e..5514c2b09 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,10 @@ app/gradle/wrapper/gradle-wrapper.jar app/gradlew app/gradlew.bat app/gradle/wrapper/gradle-wrapper.properties + +#related to OpenCV +/libraries/opencv/build +app/src/main/jniLibs +#Below removes all the HTML files related to OpenCV documentation. The documentation can be otherwise found at: +#https://docs.opencv.org/3.3.0/ +/libraries/opencv/javadoc/ diff --git a/.travis.yml b/.travis.yml index debcd6f5d..20c5bfaee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ 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) diff --git a/CHANGELOG.md b/CHANGELOG.md index a25d9fe2b..035835839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Wikimedia Commons for Android +## 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 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 29264b99d..a4a4ae0f5 100644 --- a/CREDITS +++ b/CREDITS @@ -39,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/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..f58c9225b --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +## Description + +Fixes #{GitHub issue number} + +{Describe the changes made and why they were made.} + +## Screenshots showing what changed + +{Only for user interface changes, otherwise remove this section. See [how to take a screenshot](https://android.stackexchange.com/questions/1759/how-to-take-a-screenshot-with-an-android-device)} diff --git a/app/build.gradle b/app/build.gradle index 184e756ac..6f225dc2f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,16 +18,17 @@ dependencies { 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'){ + implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.4.1@aar'){ transitive=true } - implementation "com.android.support:support-v4:${project.supportLibVersion}" - implementation "com.android.support:appcompat-v7:${project.supportLibVersion}" - implementation "com.android.support:design:${project.supportLibVersion}" + 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" + implementation "com.android.support:customtabs:$SUPPORT_LIB_VERSION" - implementation "com.android.support:cardview-v7:${project.supportLibVersion}" + implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION" implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" @@ -38,13 +39,33 @@ dependencies { 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' + + compile 'com.facebook.fresco:fresco:1.3.0' + compile 'com.facebook.stetho:stetho:1.5.0' + + testCompile 'junit:junit:4.12' + testCompile 'org.robolectric:robolectric:3.7.1' + + testCompile 'com.squareup.okhttp3:mockwebserver:3.8.1' + androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.8.1' + androidTestCompile "com.android.support:support-annotations:${project.SUPPORT_LIB_VERSION}" + androidTestCompile 'com.android.support.test.espresso:espresso-core:3.0.1' + + 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' 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' - implementation 'com.facebook.fresco:fresco:1.3.0' + implementation 'com.facebook.fresco:fresco:1.5.0' implementation 'com.facebook.stetho:stetho:1.5.0' implementation "com.google.dagger:dagger:$DAGGER_VERSION" @@ -57,22 +78,22 @@ dependencies { androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" testImplementation 'junit:junit:4.12' - testImplementation 'org.robolectric:robolectric:3.4' + testImplementation 'org.robolectric:robolectric:3.7.1' 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:${project.supportLibVersion}" + 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:1.5.1' - releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' - testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.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:2.11' - implementation 'com.google.dagger:dagger-android-support:2.11' - annotationProcessor 'com.google.dagger:dagger-compiler:2.11' - annotationProcessor 'com.google.dagger:dagger-android-processor:2.11' + 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 { @@ -83,18 +104,25 @@ android { defaultConfig { applicationId 'fr.free.nrw.commons' - versionCode 80 - versionName '2.6.5' + 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 + + multiDexEnabled 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 { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e262e9088..253bdaea8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -87,6 +87,10 @@ android:label="@string/title_activity_nearby" android:parentActivityName=".contributions.ContributionsActivity" /> + + applicationInjector() { - return injector(); - } - - /** - * used to create injector of application component - * @return Application component of Dagger - */ - public CommonsApplicationComponent injector() { - if (component == null) { - component = DaggerCommonsApplicationComponent.builder() - .appModule(new CommonsApplicationModule(this)) - .build(); - } - return component; - } /** * clears data of current application @@ -152,9 +135,10 @@ public class CommonsApplication extends DaggerApplication { .subscribe(() -> { Timber.d("All accounts have been removed"); //TODO: fix preference manager - defaultPrefs.edit().clear().commit(); - applicationPrefs.edit().clear().commit(); - applicationPrefs.edit().putBoolean("firstrun", false).apply();otherPrefs.edit().clear().commit(); + defaultPrefs.edit().clear().apply(); + applicationPrefs.edit().clear().apply(); + applicationPrefs.edit().putBoolean("firstrun", false).apply(); + otherPrefs.edit().clear().apply(); updateAllDatabases(); logoutListener.onLogoutComplete(); @@ -168,9 +152,9 @@ public class CommonsApplication extends DaggerApplication { 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); } /** 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 69a1ea4b9..e5e1b3b1b 100644 --- a/app/src/main/java/fr/free/nrw/commons/HandlerService.java +++ b/app/src/main/java/fr/free/nrw/commons/HandlerService.java @@ -8,9 +8,9 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; -import dagger.android.DaggerService; +import fr.free.nrw.commons.di.CommonsDaggerService; -public abstract class HandlerService extends DaggerService { +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/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index 0927f0338..25e778b74 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -283,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/MediaWikiImageView.java b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java index aa7ea8a15..35d197782 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java @@ -14,6 +14,7 @@ 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; @@ -38,6 +39,10 @@ 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); @@ -63,8 +68,15 @@ public class MediaWikiImageView extends SimpleDraweeView { super.onDetachedFromWindow(); } + /** + * Initializes MediaWikiImageView. + */ private void init() { - ((CommonsApplication) getContext().getApplicationContext()).injector().inject(this); + ApplicationlessInjection + .getInstance(getContext() + .getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); setHierarchy(GenericDraweeHierarchyBuilder .newInstance(getResources()) .setPlaceholderImage(VectorDrawableCompat.create(getResources(), @@ -74,6 +86,10 @@ 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); } 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 967f2cf8e..9f91bff12 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -1,8 +1,12 @@ package fr.free.nrw.commons; import android.content.Context; +import android.content.Intent; +import android.net.Uri; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.support.customtabs.CustomTabsIntent; +import android.support.v4.content.ContextCompat; import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; @@ -11,6 +15,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; +import java.net.URL; import java.net.URLEncoder; import java.util.Locale; import java.util.regex.Matcher; @@ -65,7 +70,7 @@ public class Utils { /** * Capitalizes the first character of a string. * - * @param string + * @param string String to alter * @return string with capitalized first character */ public static String capitalize(String string) { @@ -159,4 +164,15 @@ public class Utils { return stringBuilder.toString(); } + + public static void handleWebUrl(Context context,Uri url){ + CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); + builder.setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor)); + builder.setSecondaryToolbarColor(ContextCompat.getColor(context, R.color.primaryDarkColor)); + builder.setExitAnimations(context, android.R.anim.slide_in_left, android.R.anim.slide_out_right); + CustomTabsIntent customTabsIntent = builder.build(); + customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + customTabsIntent.launchUrl(context, url); + } + } 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 4114b19a9..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 @@ -8,17 +8,19 @@ import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; -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 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; public AccountUtil(Context context) { @@ -51,8 +53,8 @@ public class AccountUtil { } // 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! } private AccountManager accountManager() { 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 e793d5eb9..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,71 +1,29 @@ package fr.free.nrw.commons.auth; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerFuture; import android.os.Bundle; 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 android.accounts.AccountManager.KEY_ACCOUNT_NAME; -import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; +import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE; public abstract class AuthenticatedActivity extends NavigationBaseActivity { @Inject SessionManager sessionManager; - + @Inject + MediaWikiApi mediaWikiApi; private String authCookie; - - 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(ACCOUNT_TYPE, null, null, - null, AuthenticatedActivity.this, null, null)) - .subscribeOn(Schedulers.io()) - .map(AccountManagerFuture::getResult) - .doOnEvent((bundle, throwable) -> { - if (!bundle.containsKey(KEY_ACCOUNT_NAME)) { - throw new RuntimeException("Bundle doesn't contain account-name key: " - + KEY_ACCOUNT_NAME); - } - }) - .map(bundle -> bundle.getString(KEY_ACCOUNT_NAME)) - .doOnError(Timber::e) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(s -> { - Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE); - 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 = sessionManager.getCurrentAccount(); - if (curAccount == null) { - addAccount(accountManager); - } else { - getAuthCookie(curAccount, accountManager); + authCookie = sessionManager.getAuthCookie(); + if (authCookie != null) { + onAuthCookieAcquired(authCookie); } } @@ -74,14 +32,14 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity { super.onCreate(savedInstanceState); 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 d17120106..cbdff2e0b 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,6 +1,10 @@ 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.Activity; import android.app.ProgressDialog; import android.content.Intent; import android.content.SharedPreferences; @@ -8,37 +12,48 @@ 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.util.Log; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; +import java.io.IOException; + import javax.inject.Inject; import javax.inject.Named; import butterknife.BindView; import butterknife.ButterKnife; -import dagger.android.AndroidInjection; import fr.free.nrw.commons.BuildConfig; 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 { @@ -57,6 +72,8 @@ public class LoginActivity extends AccountAuthenticatorActivity { @BindView(R.id.loginTwoFactor) EditText twoFactorEdit; @BindView(R.id.error_message_container) ViewGroup errorMessageContainer; @BindView(R.id.error_message) TextView errorMessage; + @BindView(R.id.login_credentials) TextView loginCredentials; + @BindView(R.id.two_factor_container)TextInputLayout twoFactorContainer; ProgressDialog progressDialog; private AppCompatDelegate delegate; private LoginTextWatcher textWatcher = new LoginTextWatcher(); @@ -66,22 +83,51 @@ public class LoginActivity extends AccountAuthenticatorActivity { setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme); getDelegate().installViewFactory(); getDelegate().onCreate(savedInstanceState); - AndroidInjection.inject(this); + super.onCreate(savedInstanceState); + ApplicationlessInjection + .getInstance(this.getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); setContentView(R.layout.activity_login); ButterKnife.bind(this); usernameEdit.addTextChangedListener(textWatcher); + usernameEdit.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + hideKeyboard(v); + } + }); + passwordEdit.addTextChangedListener(textWatcher); + passwordEdit.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + hideKeyboard(v); + } + }); + twoFactorEdit.addTextChangedListener(textWatcher); passwordEdit.setOnEditorActionListener(newLoginInputActionListener()); loginButton.setOnClickListener(view -> performLogin()); signupButton.setOnClickListener(view -> signUp()); + + if(BuildConfig.FLAVOR == "beta"){ + loginCredentials.setText(getString(R.string.login_credential)); + } else { + loginCredentials.setVisibility(View.GONE); + } } + + public void hideKeyboard(View view) { + InputMethodManager inputMethodManager =(InputMethodManager)this.getSystemService(Activity.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); @@ -117,14 +163,109 @@ public class LoginActivity extends AccountAuthenticatorActivity { super.onDestroy(); } - private LoginTask getLoginTask() { - return new LoginTask( - this, - canonicializeUsername(usernameEdit.getText().toString()), - passwordEdit.getText().toString(), - twoFactorEdit.getText().toString(), - accountUtil, mwApi, defaultPrefs - ); + private void performLogin() { + Timber.d("Login to start!"); + 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 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); + } } /** @@ -176,12 +317,10 @@ public class LoginActivity extends AccountAuthenticatorActivity { } public void askUserForTwoFactorAuth() { - if (BuildConfig.DEBUG) { - twoFactorEdit.setVisibility(View.VISIBLE); - showMessageAndCancelDialog(R.string.login_failed_2fa_needed); - } else { - showMessageAndCancelDialog(R.string.login_failed_2fa_not_supported); - } + progressDialog.dismiss(); + twoFactorContainer.setVisibility(VISIBLE); + twoFactorEdit.setVisibility(VISIBLE); + showMessageAndCancelDialog(R.string.login_failed_2fa_needed); } public void showMessageAndCancelDialog(@StringRes int resId) { @@ -204,12 +343,6 @@ public class LoginActivity extends AccountAuthenticatorActivity { finish(); } - private void performLogin() { - Timber.d("Login to start!"); - LoginTask task = getLoginTask(); - task.execute(); - } - private void signUp() { Intent intent = new Intent(this, SignupActivity.class); startActivity(intent); @@ -233,7 +366,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { private void showMessage(@StringRes int resId, @ColorRes int colorResId) { errorMessage.setText(getString(resId)); errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); - errorMessageContainer.setVisibility(View.VISIBLE); + errorMessageContainer.setVisibility(VISIBLE); } private AppCompatDelegate getDelegate() { @@ -255,7 +388,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { @Override public void afterTextChanged(Editable editable) { boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 - && (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE); + && (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 b751dfe93..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java +++ /dev/null @@ -1,128 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.AccountAuthenticatorResponse; -import android.app.ProgressDialog; -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.os.Bundle; - -import java.io.IOException; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.mwapi.MediaWikiApi; -import timber.log.Timber; - -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 fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; - -class LoginTask extends AsyncTask { - - private LoginActivity loginActivity; - private String username; - private String password; - private String twoFactorCode = ""; - private AccountUtil accountUtil; - private MediaWikiApi mwApi; - - public LoginTask(LoginActivity loginActivity, String username, String password, - String twoFactorCode, AccountUtil accountUtil, - MediaWikiApi mwApi, SharedPreferences prefs) { - this.loginActivity = loginActivity; - this.username = username; - this.password = password; - this.twoFactorCode = twoFactorCode; - this.accountUtil = accountUtil; - this.mwApi = mwApi; - } - - @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 mwApi.login(username, password); - } else { - return mwApi.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!"); - - if (result.equals("PASS")) { - handlePassResult(); - } else { - handleOtherResults(result); - } - } - - private void handlePassResult() { - loginActivity.showSuccessAndDismissDialog(); - - AccountAuthenticatorResponse response = null; - - Bundle extras = loginActivity.getIntent().getExtras(); - if (extras != null) { - Timber.d("Bundle of extras: %s", extras); - response = extras.getParcelable(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); - if (response != null) { - Bundle authResult = new Bundle(); - authResult.putString(KEY_ACCOUNT_NAME, username); - authResult.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); - 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.showMessageAndCancelDialog(R.string.login_failed_network); - } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { - // Matches nosuchuser, nosuchusershort, noname - loginActivity.showMessageAndCancelDialog(R.string.login_failed_username); - loginActivity.emptySensitiveEditFields(); - } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { - // Matches wrongpassword, wrongpasswordempty - loginActivity.showMessageAndCancelDialog(R.string.login_failed_password); - loginActivity.emptySensitiveEditFields(); - } else if (result.toLowerCase().contains("throttle".toLowerCase())) { - // Matches unknown throttle error codes - loginActivity.showMessageAndCancelDialog(R.string.login_failed_throttled); - } else if (result.toLowerCase().contains("userblocked".toLowerCase())) { - // Matches login-userblocked - loginActivity.showMessageAndCancelDialog(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.showMessageAndCancelDialog(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 index 779b73b34..a7e62c34e 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java @@ -2,15 +2,13 @@ package fr.free.nrw.commons.auth; import android.accounts.Account; import android.accounts.AccountManager; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; import android.content.Context; - -import java.io.IOException; +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; @@ -21,11 +19,13 @@ 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) { + public SessionManager(Context context, MediaWikiApi mediaWikiApi, SharedPreferences sharedPreferences) { this.context = context; this.mediaWikiApi = mediaWikiApi; this.currentAccount = null; + this.sharedPreferences = sharedPreferences; } /** @@ -51,14 +51,28 @@ public class SessionManager { } accountManager.invalidateAuthToken(ACCOUNT_TYPE, mediaWikiApi.getAuthCookie()); - try { - String authCookie = accountManager.blockingGetAuthToken(curAccount, "", false); - mediaWikiApi.setAuthCookie(authCookie); - return true; - } catch (OperationCanceledException | NullPointerException | IOException | AuthenticatorException e) { - e.printStackTrace(); + 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() { 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 c08e27966..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,53 +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.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.AccountUtil.ACCOUNT_TYPE; -import static fr.free.nrw.commons.auth.LoginActivity.PARAM_USERNAME; +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}; + @NonNull private final Context context; - private MediaWikiApi mediaWikiApi; - WikiAccountAuthenticator(Context context, MediaWikiApi mwApi) { + public WikiAccountAuthenticator(@NonNull Context context) { super(context); this.context = context; - this.mediaWikiApi = mwApi; } - 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 ACCOUNT_TYPE.equals(type); - } - @Override public Bundle addAccount(@NonNull AccountAuthenticatorResponse response, @NonNull String accountType, @Nullable String authTokenType, @@ -59,86 +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 { - //TODO add 2fa support here - String result = mediaWikiApi.login(username, password); - if (result.equals("PASS")) { - return mediaWikiApi.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, ACCOUNT_TYPE); - 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 @@ -147,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 b0f3e6063..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,30 +1,26 @@ package fr.free.nrw.commons.auth; +import android.accounts.AbstractAccountAuthenticator; import android.content.Intent; import android.os.IBinder; +import android.support.annotation.Nullable; -import javax.inject.Inject; +import fr.free.nrw.commons.di.CommonsDaggerService; -import dagger.android.DaggerService; -import fr.free.nrw.commons.mwapi.MediaWikiApi; +public class WikiAccountAuthenticatorService extends CommonsDaggerService { -import static android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT; - -public class WikiAccountAuthenticatorService extends DaggerService { - - @Inject MediaWikiApi mwApi; - private WikiAccountAuthenticator wikiAccountAuthenticator = null; + @Nullable + private AbstractAccountAuthenticator authenticator; @Override - public IBinder onBind(Intent intent) { - if (!intent.getAction().equals(ACTION_AUTHENTICATOR_INTENT)) { - return null; - } - - if (wikiAccountAuthenticator == null) { - wikiAccountAuthenticator = new WikiAccountAuthenticator(this, mwApi); - } - 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 0c8e2a877..c826013f0 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,18 +1,22 @@ package fr.free.nrw.commons.category; -import android.content.ContentProviderClient; +import android.app.Activity; import android.content.SharedPreferences; import android.os.Bundle; import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.text.Editable; import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; @@ -34,11 +38,11 @@ import javax.inject.Named; import butterknife.BindView; import butterknife.ButterKnife; -import dagger.android.support.DaggerFragment; 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.upload.SingleUploadFragment; import fr.free.nrw.commons.utils.StringSortingUtils; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -47,12 +51,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 DaggerFragment { +public class CategorizationFragment extends CommonsDaggerSupportFragment { public static final int SEARCH_CATS_LIMIT = 25; @@ -69,17 +72,18 @@ public class CategorizationFragment extends DaggerFragment { @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 TitleTextWatcher textWatcher = new TitleTextWatcher(); private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> { if (item.isSelected()) { selectedCategories.add(item); - updateCategoryCount(item, databaseClient); + updateCategoryCount(item); } else { selectedCategories.remove(item); } @@ -105,6 +109,15 @@ public class CategorizationFragment extends DaggerFragment { categoriesAdapter = adapterFactory.create(items); categoriesList.setAdapter(categoriesAdapter); + + categoriesFilter.addTextChangedListener(textWatcher); + + categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + hideKeyboard(v); + } + }); + RxTextView.textChanges(categoriesFilter) .takeUntil(RxView.detaches(categoriesFilter)) .debounce(500, TimeUnit.MILLISECONDS) @@ -113,6 +126,18 @@ public class CategorizationFragment extends DaggerFragment { return rootView; } + public void hideKeyboard(View view) { + InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + @Override + public void onDestroyView() { + categoriesFilter.removeTextChangedListener(textWatcher); + super.onDestroyView(); + } + + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { menu.clear(); @@ -137,12 +162,6 @@ public class CategorizationFragment extends DaggerFragment { } } - @Override - public void onDestroy() { - super.onDestroy(); - databaseClient.release(); - } - @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -178,7 +197,6 @@ public class CategorizationFragment extends DaggerFragment { setHasOptionsMenu(true); onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity(); getActivity().setTitle(R.string.categories_activity_title); - databaseClient = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY); } private void updateCategoryList(String filter) { @@ -261,7 +279,7 @@ public class CategorizationFragment extends DaggerFragment { } 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)); } @@ -307,28 +325,22 @@ public class CategorizationFragment extends DaggerFragment { //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() { @@ -367,4 +379,21 @@ public class CategorizationFragment extends DaggerFragment { .create() .show(); } + + private class TitleTextWatcher implements TextWatcher { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { + } + + @Override + public void afterTextChanged(Editable editable) { + if (getActivity() != null) { + getActivity().invalidateOptionsMenu(); + } + } + } } 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 ed698ec4c..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; @@ -12,16 +11,16 @@ import android.text.TextUtils; import javax.inject.Inject; -import dagger.android.AndroidInjection; 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 @@ -44,12 +43,6 @@ public class CategoryContentProvider extends ContentProvider { @Inject DBOpenHelper dbOpenHelper; - @Override - public boolean onCreate() { - AndroidInjection.inject(this); - return false; - } - @SuppressWarnings("ConstantConditions") @Override public Cursor query(@NonNull Uri uri, String[] projection, String selection, diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java new file mode 100644 index 000000000..e63b04c26 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java @@ -0,0 +1,184 @@ +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.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 javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; + +public class CategoryDao { + + private final Provider clientProvider; + + @Inject + public CategoryDao(@Named("category") Provider clientProvider) { + this.clientProvider = clientProvider; + } + + public void save(Category category) { + ContentProviderClient db = clientProvider.get(); + try { + if (category.getContentUri() == null) { + category.setContentUri(db.insert(CategoryContentProvider.BASE_URI, toContentValues(category))); + } else { + db.update(category.getContentUri(), toContentValues(category), null, null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + db.release(); + } + } + + /** + * Find persisted category in database, based on its name. + * + * @param name Category's name + * @return category from database, or null if not found + */ + @Nullable + Category find(String name) { + Cursor cursor = null; + ContentProviderClient db = clientProvider.get(); + try { + cursor = db.query( + CategoryContentProvider.BASE_URI, + Table.ALL_FIELDS, + Table.COLUMN_NAME + "=?", + new String[]{name}, + null); + if (cursor != null && cursor.moveToFirst()) { + return fromCursor(cursor); + } + } catch (RemoteException e) { + // This feels lazy, but to hell with checked exceptions. :) + throw new RuntimeException(e); + } finally { + if (cursor != null) { + cursor.close(); + } + db.release(); + } + return null; + } + + /** + * Retrieve recently-used categories, ordered by descending date. + * + * @return a list containing recent categories + */ + @NonNull + List recentCategories(int limit) { + List items = new ArrayList<>(); + Cursor cursor = null; + ContentProviderClient db = clientProvider.get(); + try { + cursor = db.query( + CategoryContentProvider.BASE_URI, + Table.ALL_FIELDS, + null, + new String[]{}, + 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) { + items.add(fromCursor(cursor).getName()); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + 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"; + 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 = { + COLUMN_ID, + COLUMN_NAME, + COLUMN_LAST_USED, + COLUMN_TIMES_USED + }; + + 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," + + COLUMN_TIMES_USED + " INTEGER" + + ");"; + + 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 < 4) { + // doesn't exist yet + from++; + onUpdate(db, from, to); + return; + } + if (from == 4) { + // table added in version 5 + onCreate(db); + from++; + onUpdate(db, from, to); + return; + } + if (from == 5) { + from++; + onUpdate(db, from, to); + return; + } + } + } +} 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 e673c7d9d..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,14 +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.support.annotation.NonNull; -import android.text.TextUtils; import java.text.SimpleDateFormat; import java.util.Date; @@ -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; } @@ -155,62 +172,6 @@ public class Contribution extends Media { 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; } @@ -263,119 +197,8 @@ public class Contribution extends Media { this.localUri = localUri; } - 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); - } - - 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; - } - } + public void setDecimalCoords(String decimalCoords) { + this.decimalCoords = decimalCoords; } @NonNull @@ -396,6 +219,7 @@ public class Contribution extends Media { case Prefs.Licenses.CC_BY_SA: return "{{self|cc-by-sa-3.0}}"; } + 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 92cf063d4..09b04b24f 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 @@ -80,11 +80,20 @@ public class ContributionController { //FIXME: Starts gallery (opens Google Photos) Intent pickImageIntent = new Intent(ACTION_GET_CONTENT); pickImageIntent.setType("image/*"); +<<<<<<< HEAD Timber.d("startGalleryPick() called with pickImageIntent"); // See https://stackoverflow.com/questions/22366596/android-illegalstateexception-fragment-not-attached-to-activity-webview if (!fragment.isAdded()) { return; } +======= + // See https://stackoverflow.com/questions/22366596/android-illegalstateexception-fragment-not-attached-to-activity-webview + if (!fragment.isAdded()) { + Timber.d("Fragment is not added, startActivityForResult cannot be called"); + return; + } + Timber.d("startGalleryPick() called with pickImageIntent"); +>>>>>>> directNearbyUploadsNewLocal fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY); } @@ -105,12 +114,20 @@ public class ContributionController { } 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); if (isDirectUpload) { shareIntent.putExtra("isDirectUpload", true); } +<<<<<<< HEAD +======= + break; + default: +>>>>>>> directNearbyUploadsNewLocal break; } Timber.i("Image selected"); @@ -132,5 +149,4 @@ public class ContributionController { lastGeneratedCaptureUri = savedInstanceState.getParcelable("lastGeneratedCaptureURI"); } } - } 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 6cda47d2e..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 @@ -42,8 +42,7 @@ 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; @@ -58,6 +57,7 @@ public class ContributionsActivity @Inject MediaWikiApi mediaWikiApi; @Inject SessionManager sessionManager; @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject ContributionDao contributionDao; private Cursor allContributions; private ContributionsListFragment contributionsList; @@ -65,21 +65,6 @@ public class ContributionsActivity 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(); @@ -94,7 +79,7 @@ public class ContributionsActivity @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!")); } }; @@ -121,14 +106,13 @@ public class ContributionsActivity @Override protected void onAuthCookieAcquired(String authCookie) { // Do a sync everytime we get here! - requestSync(sessionManager.getCurrentAccount(), ContributionsContentProvider.AUTHORITY, new Bundle()); + 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); } @@ -186,24 +170,23 @@ public class ContributionsActivity 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()); } } @@ -239,8 +222,8 @@ public class ContributionsActivity public Loader onCreateLoader(int i, Bundle bundle) { 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 @@ -249,7 +232,7 @@ public class ContributionsActivity if (contributionsList.getAdapter() == null) { contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(), - cursor, 0)); + cursor, 0, contributionDao)); } else { ((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor); } @@ -270,7 +253,7 @@ public class ContributionsActivity // 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)); } } 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 5ec290026..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; @@ -12,27 +11,27 @@ import android.text.TextUtils; import javax.inject.Inject; -import dagger.android.AndroidInjection; 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) { @@ -41,12 +40,6 @@ public class ContributionsContentProvider extends ContentProvider { @Inject DBOpenHelper dbOpenHelper; - @Override - public boolean onCreate() { - AndroidInjection.inject(this); - return true; - } - @SuppressWarnings("ConstantConditions") @Override public Cursor query(@NonNull Uri uri, String[] projection, String selection, @@ -93,7 +86,7 @@ public class ContributionsContentProvider extends ContentProvider { public Uri insert(@NonNull Uri uri, ContentValues contentValues) { int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - long id = 0; + long id; switch (uriType) { case CONTRIBUTIONS: id = sqlDB.insert(TABLE_NAME, null, contentValues); @@ -165,7 +158,7 @@ public class ContributionsContentProvider extends ContentProvider { */ int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - int rowsUpdated = 0; + int rowsUpdated; switch (uriType) { case CONTRIBUTIONS: rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, selection, selectionArgs); @@ -176,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 7be6dd663..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()); 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 f58e6a9ab..0febe8dcb 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 @@ -20,13 +20,15 @@ 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 dagger.android.support.DaggerFragment; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.nearby.NearbyActivity; import timber.log.Timber; @@ -36,7 +38,7 @@ import static android.app.Activity.RESULT_OK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.view.View.GONE; -public class ContributionsListFragment extends DaggerFragment { +public class ContributionsListFragment extends CommonsDaggerSupportFragment { @BindView(R.id.contributionsList) GridView contributionsList; @@ -45,8 +47,12 @@ public class ContributionsListFragment extends DaggerFragment { @BindView(R.id.loadingContributionsProgressBar) ProgressBar progressBar; - @Inject @Named("prefs") SharedPreferences prefs; - @Inject @Named("default_preferences") SharedPreferences defaultPrefs; + @Inject + @Named("prefs") + SharedPreferences prefs; + @Inject + @Named("default_preferences") + SharedPreferences defaultPrefs; private ContributionController controller; @@ -208,7 +214,7 @@ public class ContributionsListFragment extends DaggerFragment { 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 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 e67b164a8..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 @@ -23,14 +23,14 @@ 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 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") @@ -81,7 +81,11 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { @Override public void onPerformSync(Account account, Bundle bundle, String authority, ContentProviderClient contentProviderClient, SyncResult syncResult) { - ((CommonsApplication) getContext().getApplicationContext()).injector().inject(this); + 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; String lastModified = prefs.getString("lastSyncTimestamp", ""); @@ -89,6 +93,7 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter { LogEventResult result; Boolean done = false; String queryContinue = null; + ContributionDao contributionDao = new ContributionDao(() -> contentProviderClient); while (!done) { try { @@ -121,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 { diff --git a/app/src/main/java/fr/free/nrw/commons/data/Category.java b/app/src/main/java/fr/free/nrw/commons/data/Category.java deleted file mode 100644 index be4a29846..000000000 --- a/app/src/main/java/fr/free/nrw/commons/data/Category.java +++ /dev/null @@ -1,276 +0,0 @@ -package fr.free.nrw.commons.data; - -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 fr.free.nrw.commons.category.CategoryContentProvider; - -/** - * Represents a category - */ -public class Category { - private Uri contentUri; - - private String name; - private Date lastUsed; - private int timesUsed; - - // Getters/setters - /** - * 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 - */ - private Date getLastUsed() { - // warning: Date objects are mutable. - return (Date)lastUsed.clone(); - } - - /** - * Modifies last used date - * - * @param lastUsed Category date - */ - public void setLastUsed(Date lastUsed) { - // warning: Date objects are mutable. - this.lastUsed = (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 - */ - private int getTimesUsed() { - return timesUsed; - } - - /** - * Modifies no. of times used - * - * @param timesUsed Category used times - */ - public void setTimesUsed(int timesUsed) { - this.timesUsed = timesUsed; - } - - /** - * Increments timesUsed by 1 and sets last used date as now. - */ - public void incTimesUsed() { - timesUsed++; - touch(); - } - - //region Database/content-provider stuff - - /** - * Persist category. - * @param client ContentProviderClient to handle DB connection - */ - public void save(ContentProviderClient client) { - try { - if (contentUri == null) { - contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues()); - } else { - client.update(contentUri, toContentValues(), null, null); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - /** - * Gets content values - * - * @return Content values - */ - 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; - } - - /** - * Gets category from cursor - * @param cursor Category cursor - * @return Category from cursor - */ - 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) { - Cursor cursor = null; - try { - cursor = client.query( - CategoryContentProvider.BASE_URI, - Category.Table.ALL_FIELDS, - Category.Table.COLUMN_NAME + "=?", - new String[]{name}, - null); - if (cursor != null && cursor.moveToFirst()) { - return Category.fromCursor(cursor); - } - } catch (RemoteException e) { - // This feels lazy, but to hell with checked exceptions. :) - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - 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<>(); - Cursor cursor = null; - try { - cursor = client.query( - CategoryContentProvider.BASE_URI, - Category.Table.ALL_FIELDS, - null, - new String[]{}, - Category.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()); - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - return items; - } - - 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"; - - // 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_NAME, - COLUMN_LAST_USED, - COLUMN_TIMES_USED - }; - - private static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + COLUMN_ID + " INTEGER PRIMARY KEY," - + COLUMN_NAME + " STRING," - + COLUMN_LAST_USED + " INTEGER," - + COLUMN_TIMES_USED + " INTEGER" - + ");"; - - /** - * Creates new table with provided SQLite database - * - * @param db Category database - */ - public static void onCreate(SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - /** - * Deletes existing table - * @param db Category database - */ - public static void onDelete(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); - onCreate(db); - } - - /** - * Updates given database - * @param db Category database - * @param from Exiting category id - * @param to New category id - */ - public static void onUpdate(SQLiteDatabase db, int from, int to) { - if (from == to) { - return; - } - if (from < 4) { - // doesn't exist yet - from++; - onUpdate(db, from, to); - return; - } - if (from == 4) { - // table added in version 5 - onCreate(db); - from++; - onUpdate(db, from, to); - return; - } - if (from == 5) { - from++; - onUpdate(db, from, to); - return; - } - } - } - //endregion -} \ No newline at end of file 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 5e28a8a32..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,8 +4,9 @@ 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 { @@ -13,7 +14,8 @@ public class DBOpenHelper extends SQLiteOpenHelper { 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 index 27e16ce23..e4fb13427 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -8,6 +8,7 @@ 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; @@ -43,4 +44,6 @@ public abstract class ActivityBuilderModule { @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 index 2881f33fd..080f61b61 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -8,8 +8,11 @@ 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; +import fr.free.nrw.commons.nearby.PlaceRenderer; @Singleton @Component(modules = { @@ -21,7 +24,7 @@ import fr.free.nrw.commons.modifications.ModificationsSyncAdapter; ServiceBuilderModule.class, ContentProviderBuilderModule.class }) -public interface CommonsApplicationComponent extends AndroidInjector { +public interface CommonsApplicationComponent extends AndroidInjector { void inject(CommonsApplication application); void inject(ContributionsSyncAdapter syncAdapter); @@ -30,6 +33,14 @@ public interface CommonsApplicationComponent extends AndroidInjector>>>>>> directNearbyUploadsNewLocal } @Provides @Singleton - public SessionManager providesSessionManager(MediaWikiApi mediaWikiApi) { - return new SessionManager(application, mediaWikiApi); + public SessionManager providesSessionManager(Context context, + MediaWikiApi mediaWikiApi, + @Named("default_preferences") SharedPreferences sharedPreferences) { + return new SessionManager(context, mediaWikiApi, sharedPreferences); } @Provides @Singleton - public MediaWikiApi provideMediaWikiApi() { - return new ApacheHttpClientMediaWikiApi(BuildConfig.WIKIMEDIA_API_HOST); + public MediaWikiApi provideMediaWikiApi(Context context, @Named("default_preferences") SharedPreferences sharedPreferences) { + return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, sharedPreferences); } @Provides @Singleton - public LocationServiceManager provideLocationServiceManager() { - return new LocationServiceManager(application); + public LocationServiceManager provideLocationServiceManager(Context context) { + return new LocationServiceManager(context); } @Provides @@ -92,8 +135,8 @@ public class CommonsApplicationModule { @Provides @Singleton - public DBOpenHelper provideDBOpenHelper() { - return new DBOpenHelper(application); + public DBOpenHelper provideDBOpenHelper(Context context) { + return new DBOpenHelper(context); } @Provides @@ -107,4 +150,4 @@ public class CommonsApplicationModule { 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/location/LatLng.java b/app/src/main/java/fr/free/nrw/commons/location/LatLng.java index a4948c66f..81e9ec707 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,137 +1,162 @@ -package fr.free.nrw.commons.location; - -import android.location.Location; -import android.net.Uri; -import android.support.annotation.NonNull; - -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 static LatLng from(@NonNull Location location) { - return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy()); - } - - 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; - } - - public Uri getGmmIntentUri() { - return Uri.parse("geo:0,0?q=" + latitude + "," + longitude); - } -} \ No newline at end of file +package fr.free.nrw.commons.location; + +import android.location.Location; +import android.net.Uri; +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; + } + + public Uri getGmmIntentUri() { + return Uri.parse("geo:0,0?q=" + latitude + "," + longitude); + } +} + 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 b9e5c943c..f9a171461 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 @@ -30,20 +30,40 @@ public class LocationServiceManager implements LocationListener { 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); } + /** + * Returns the current status of the GPS provider. + * + * @return true if the GPS provider is enabled + */ public boolean isProviderEnabled() { return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); } + /** + * 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; @@ -54,11 +74,9 @@ public class LocationServiceManager implements LocationListener { } public boolean isPermissionExplanationRequired(Activity activity) { - if (activity.isFinishing()) { - return false; - } - return ActivityCompat.shouldShowRequestPermissionRationale(activity, - Manifest.permission.ACCESS_FINE_LOCATION); + return !activity.isFinishing() && + ActivityCompat.shouldShowRequestPermissionRationale(activity, + Manifest.permission.ACCESS_FINE_LOCATION); } public LatLng getLastLocation() { @@ -68,7 +86,8 @@ public class LocationServiceManager implements LocationListener { return LatLng.from(lastLocation); } - /** Registers a LocationManager to listen for current location. + /** + * Registers a LocationManager to listen for current location. */ public void registerLocationManager() { if (!isLocationManagerRegistered) @@ -76,6 +95,12 @@ public class LocationServiceManager implements LocationListener { && 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(locationProvider, @@ -92,7 +117,16 @@ public class LocationServiceManager implements LocationListener { } } + /** + * 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 LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly + * LOCATION_SLIGHTLY_CHANGED if location changed slightly + */ protected LocationChangeType isBetterLocation(Location location, Location currentBestLocation) { + if (currentBestLocation == null) { // A new location is always better than no location return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; @@ -148,7 +182,8 @@ public class LocationServiceManager implements LocationListener { return provider1.equals(provider2); } - /** Unregisters location manager. + /** + * Unregisters location manager. */ public void unregisterLocationManager() { isLocationManagerRegistered = false; @@ -159,12 +194,22 @@ 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); } 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 75e366767..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 @@ -24,7 +24,6 @@ import java.util.Locale; import javax.inject.Inject; import javax.inject.Provider; -import dagger.android.support.DaggerFragment; import fr.free.nrw.commons.License; import fr.free.nrw.commons.LicenseList; import fr.free.nrw.commons.Media; @@ -32,12 +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.mwapi.MediaWikiApi; import fr.free.nrw.commons.ui.widget.CompatTextView; import timber.log.Timber; -public class MediaDetailFragment extends DaggerFragment { +public class MediaDetailFragment extends CommonsDaggerSupportFragment { private boolean editable; private MediaDetailPagerFragment.MediaDetailProvider detailProvider; @@ -77,7 +76,7 @@ public class MediaDetailFragment extends DaggerFragment { private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once! private ViewTreeObserver.OnScrollChangedListener scrollListener; private DataSetObserver dataObserver; - private AsyncTask detailFetchTask; + private AsyncTask detailFetchTask; private LicenseList licenseList; @Override @@ -96,7 +95,7 @@ public class MediaDetailFragment extends DaggerFragment { @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"); @@ -157,7 +156,8 @@ public class MediaDetailFragment extends DaggerFragment { return view; } - @Override public void onResume() { + @Override + public void onResume() { super.onResume(); Media media = detailProvider.getMediaAtPosition(index); if (media == null) { @@ -239,13 +239,13 @@ public class MediaDetailFragment extends DaggerFragment { 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); @@ -290,7 +290,7 @@ public class MediaDetailFragment extends DaggerFragment { 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) { @@ -309,7 +309,7 @@ public class MediaDetailFragment extends DaggerFragment { // 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; @@ -363,7 +363,8 @@ public class MediaDetailFragment extends DaggerFragment { } - private @Nullable String licenseLink(Media media) { + private @Nullable + String licenseLink(Media media) { String licenseKey = media.getLicense(); if (licenseKey == null || licenseKey.equals("")) { return 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 5d6e66faf..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 @@ -28,12 +28,12 @@ import android.view.ViewGroup; import javax.inject.Inject; import javax.inject.Named; -import dagger.android.support.DaggerFragment; 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.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.mwapi.MediaWikiApi; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; @@ -41,11 +41,15 @@ import static android.content.Context.DOWNLOAD_SERVICE; import static android.content.Intent.ACTION_VIEW; import static android.content.pm.PackageManager.PERMISSION_GRANTED; -public class MediaDetailPagerFragment extends DaggerFragment implements ViewPager.OnPageChangeListener { +public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener { - @Inject MediaWikiApi mwApi; - @Inject SessionManager sessionManager; - @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject + MediaWikiApi mwApi; + @Inject + SessionManager sessionManager; + @Inject + @Named("default_preferences") + SharedPreferences prefs; private ViewPager pager; private Boolean editable; @@ -164,13 +168,19 @@ public class MediaDetailPagerFragment extends DaggerFragment implements ViewPage 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/ModificationsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java index 6dc993c9e..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; @@ -12,24 +11,26 @@ import android.text.TextUtils; import javax.inject.Inject; -import dagger.android.AndroidInjection; 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) { @@ -38,16 +39,10 @@ public class ModificationsContentProvider extends ContentProvider { @Inject DBOpenHelper dbOpenHelper; - @Override - public boolean onCreate() { - AndroidInjection.inject(this); - return true; - } - @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); @@ -75,10 +70,10 @@ public class ModificationsContentProvider extends ContentProvider { public Uri insert(@NonNull Uri uri, ContentValues contentValues) { int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - long id = 0; + 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); @@ -94,7 +89,7 @@ public class ModificationsContentProvider extends ContentProvider { switch (uriType) { case MODIFICATIONS_ID: String id = uri.getLastPathSegment(); - sqlDB.delete(ModifierSequence.Table.TABLE_NAME, + sqlDB.delete(TABLE_NAME, "_id = ?", new String[] { id } ); @@ -114,7 +109,7 @@ public class ModificationsContentProvider extends ContentProvider { case MODIFICATIONS: 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: @@ -137,10 +132,10 @@ public class ModificationsContentProvider extends ContentProvider { */ int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - int rowsUpdated = 0; + int rowsUpdated; switch (uriType) { case MODIFICATIONS: - rowsUpdated = sqlDB.update(ModifierSequence.Table.TABLE_NAME, + rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, selection, selectionArgs); @@ -149,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 1e886f225..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; @@ -16,15 +13,21 @@ import java.io.IOException; import javax.inject.Inject; -import fr.free.nrw.commons.CommonsApplication; +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); @@ -33,7 +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! - ((CommonsApplication)getContext().getApplicationContext()).injector().inject(this); + ApplicationlessInjection + .getInstance(getContext() + .getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); Cursor allModifications; try { @@ -48,16 +55,7 @@ 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) { - Timber.d("Could not authenticate :("); - return; - } - + String authCookie = sessionManager.getAuthCookie(); if (isNullOrWhiteSpace(authCookie)) { Timber.d("Could not authenticate :("); return; @@ -79,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 = mwApi.revisionsByFilename(contrib.getFilename()); } catch (IOException e) { - Timber.d("Network fuckup on modifications sync!"); + Timber.d("Network messed up on modifications sync!"); continue; } @@ -111,17 +117,17 @@ public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter { try { 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(); 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 880b53313..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,14 +11,13 @@ 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++) { @@ -32,7 +25,7 @@ public class ModifierSequence { } } - public Uri getMediaUri() { + Uri getMediaUri() { return mediaUri; } @@ -40,14 +33,14 @@ 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) { editSummary.append(modifier.getEditSumary()).append(" "); @@ -56,97 +49,16 @@ public class ModifierSequence { 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/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 19425a0c3..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,6 +23,8 @@ 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; @@ -36,11 +40,17 @@ import java.util.concurrent.Callable; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.PageTitle; +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 */ @@ -50,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 @@ -75,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()); } @@ -92,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()); } @@ -122,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"; } @@ -137,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(); @@ -353,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") 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/nearby/DirectUpload.java b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java index b6602774c..b58afa82a 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java @@ -13,16 +13,14 @@ import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; -public class DirectUpload { +class DirectUpload { private ContributionController controller; private Fragment fragment; - private SharedPreferences prefs; - DirectUpload(Fragment fragment, ContributionController controller, SharedPreferences prefs) { + DirectUpload(Fragment fragment, ContributionController controller) { this.fragment = fragment; this.controller = controller; - this.prefs = prefs; } void initiateCameraUpload() { 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 7d5917f88..344b415b7 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 @@ -7,7 +7,9 @@ import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.design.widget.BottomSheetBehavior; + import android.support.v4.app.FragmentTransaction; +import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.AlertDialog; import android.util.Log; import android.view.Menu; @@ -71,6 +73,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName(); private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName(); + @BindView(R.id.swipe_container) SwipeRefreshLayout swipeLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -78,6 +81,8 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp ButterKnife.bind(this); resumeFragment(); bundle = new Bundle(); + + initBottomSheetBehaviour(); initDrawer(); } @@ -90,6 +95,14 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp private void initBottomSheetBehaviour() { transparentView.setAlpha(0); + swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + lockNearbyView(false); + refreshView(true); + } + }); + bottomSheet.getLayoutParams().height = getWindowManager() .getDefaultDisplay().getHeight() / 16 * 9; bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapterFactory.java index edc805928..f596dc05a 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapterFactory.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyAdapterFactory.java @@ -1,5 +1,8 @@ package fr.free.nrw.commons.nearby; + +import android.support.v4.app.Fragment; + import com.pedrogomez.renderers.ListAdapteeCollection; import com.pedrogomez.renderers.RVRendererAdapter; import com.pedrogomez.renderers.RendererBuilder; @@ -7,11 +10,25 @@ import com.pedrogomez.renderers.RendererBuilder; import java.util.Collections; import java.util.List; +import fr.free.nrw.commons.contributions.ContributionController; + class NearbyAdapterFactory { + private Fragment fragment; + private ContributionController controller; + + NearbyAdapterFactory(){ + + } + + NearbyAdapterFactory(Fragment fragment, ContributionController controller) { + this.fragment = fragment; + this.controller = controller; + } + public RVRendererAdapter create(List placeList) { RendererBuilder builder = new RendererBuilder() - .bind(Place.class, new PlaceRenderer()); + .bind(Place.class, new PlaceRenderer(fragment, controller)); ListAdapteeCollection collection = new ListAdapteeCollection<>( placeList != null ? placeList : Collections.emptyList()); return new RVRendererAdapter<>(builder, collection); 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 3f79ef201..0ecf09160 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 @@ -39,11 +39,13 @@ public class NearbyController { /** * Prepares Place list to make their distance information update later. + * * @param curLatLng current location for user - * @return Place list without distance information + * @return NearbyPlacesInfo a variable holds Place list without distance information + * and boundary coordinates of current Place List */ - //public List loadAttractionsFromLocation(LatLng curLatLng) { public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) { + Timber.d("Loading attractions near %s", curLatLng); NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo(); @@ -51,10 +53,12 @@ public class NearbyController { return null; } List places = nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()); + LatLng[] boundaryCoordinates = {places.get(0).location, // south places.get(0).location, // north places.get(0).location, // west places.get(0).location};// east, init with a random location + if (curLatLng != null) { Timber.d("Sorting places by distance..."); final Map distances = new HashMap<>(); @@ -89,6 +93,7 @@ public class NearbyController { /** * 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 @@ -97,7 +102,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); } @@ -105,7 +110,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 @@ -122,26 +128,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/NearbyListFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java index 495ab0977..dcc7f5e24 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,7 +1,11 @@ package fr.free.nrw.commons.nearby; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -17,12 +21,17 @@ import java.lang.reflect.Type; import java.util.Collections; import java.util.List; +import dagger.android.support.AndroidSupportInjection; import dagger.android.support.DaggerFragment; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.utils.UriDeserializer; import timber.log.Timber; +import static android.app.Activity.RESULT_OK; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + public class NearbyListFragment extends DaggerFragment { private static final Type LIST_TYPE = new TypeToken>() { }.getType(); @@ -34,6 +43,7 @@ public class NearbyListFragment extends DaggerFragment { private NearbyAdapterFactory adapterFactory; private RecyclerView recyclerView; + private ContributionController controller; @Override public void onCreate(Bundle savedInstanceState) { @@ -41,6 +51,12 @@ public class NearbyListFragment extends DaggerFragment { setRetainInstance(true); } + @Override + public void onAttach(Context context) { + AndroidSupportInjection.inject(this); + super.onAttach(context); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -49,7 +65,9 @@ public class NearbyListFragment extends DaggerFragment { View view = inflater.inflate(R.layout.fragment_nearby, container, false); recyclerView = view.findViewById(R.id.listView); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - adapterFactory = new NearbyAdapterFactory(); + + controller = new ContributionController(this); + adapterFactory = new NearbyAdapterFactory(this, controller); return view; } @@ -79,6 +97,47 @@ public class NearbyListFragment extends DaggerFragment { placeList = NearbyController.loadAttractionsFromLocationToPlaces(curLatLng, placeList); } + return placeList; } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + Timber.d("onRequestPermissionsResult: req code = " + " perm = " + permissions + " grant =" + grantResults); + + switch (requestCode) { + // 1 = "Read external storage" allowed when gallery selected + case 1: { + if (grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED) { + Timber.d("Call controller.startGalleryPick()"); + controller.startGalleryPick(); + } + } + break; + + // 3 = "Write external storage" allowed when camera selected + case 3: { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Timber.d("Call controller.startCameraCapture()"); + controller.startCameraCapture(); + } + } + } + } + + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (resultCode == RESULT_OK) { + Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", + requestCode, resultCode, data); + controller.handleImagePicked(requestCode, data, true); + } else { + Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", + requestCode, resultCode, data); + } + } + } \ No newline at end of file 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 ef33b3ca5..4e6f1a815 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,6 +3,7 @@ package fr.free.nrw.commons.nearby; import android.animation.ObjectAnimator; import android.animation.TypeEvaluator; import android.animation.ValueAnimator; + import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; @@ -15,7 +16,6 @@ import android.support.design.widget.BottomSheetBehavior; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; -import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; @@ -115,7 +115,6 @@ public class NearbyMapFragment extends DaggerFragment { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle bundle = this.getArguments(); - Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriDeserializer()) .create(); @@ -388,6 +387,8 @@ public class NearbyMapFragment extends DaggerFragment { 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) @@ -546,6 +547,8 @@ public class NearbyMapFragment extends DaggerFragment { icon.setImageResource(place.getLabel().getIcon()); + title.setText(place.name); + distance.setText(place.distance); description.setText(place.getLongDescription()); title.setText(place.name.toString()); distance.setText(place.distance.toString()); @@ -553,7 +556,8 @@ public class NearbyMapFragment extends DaggerFragment { fabCamera.setOnClickListener(view -> { Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); controller = new ContributionController(this); - DirectUpload directUpload = new DirectUpload(this, controller, prefs); + + DirectUpload directUpload = new DirectUpload(this, controller); storeSharedPrefs(); directUpload.initiateCameraUpload(); }); @@ -561,9 +565,14 @@ public class NearbyMapFragment extends DaggerFragment { fabGallery.setOnClickListener(view -> { Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); controller = new ContributionController(this); - DirectUpload directUpload = new DirectUpload(this, controller, prefs); + + DirectUpload directUpload = new DirectUpload(this, controller); storeSharedPrefs(); directUpload.initiateGalleryUpload(); + + //TODO: App crashes after image upload completes + //TODO: Handle onRequestPermissionsResult + }); } 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 242ebbb2d..0137516bb 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 @@ -32,7 +32,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); 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 ca0ae0a89..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,23 +1,31 @@ package fr.free.nrw.commons.nearby; +import android.content.Context; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import butterknife.ButterKnife; -import dagger.android.support.DaggerFragment; +import dagger.android.support.AndroidSupportInjection; import fr.free.nrw.commons.R; import timber.log.Timber; /** * Tells user that Nearby Places cannot be displayed if location permissions are denied */ -public class NoPermissionsFragment extends DaggerFragment { +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/PlaceRenderer.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java index 61c758605..0d432f90e 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 @@ -1,7 +1,10 @@ package fr.free.nrw.commons.nearby; import android.content.Intent; + import android.net.Uri; +import android.content.SharedPreferences; +import android.support.v4.app.Fragment; import android.support.transition.TransitionManager; import android.support.v7.widget.PopupMenu; import android.util.Log; @@ -17,11 +20,17 @@ import com.pedrogomez.renderers.Renderer; import java.util.ArrayList; +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.contributions.ContributionController; +import timber.log.Timber; -class PlaceRenderer extends Renderer { +public class PlaceRenderer extends Renderer { @BindView(R.id.tvName) TextView tvName; @BindView(R.id.tvDesc) TextView tvDesc; @@ -29,11 +38,13 @@ class PlaceRenderer extends Renderer { @BindView(R.id.icon) ImageView icon; @BindView(R.id.buttonLayout) LinearLayout buttonLayout; @BindView(R.id.cameraButton) LinearLayout cameraButton; - @BindView(R.id.galeryButton) LinearLayout galeryButton; + + @BindView(R.id.galleryButton) LinearLayout galleryButton; @BindView(R.id.directionsButton) LinearLayout directionsButton; @BindView(R.id.iconOverflow) LinearLayout iconOverflow; @BindView(R.id.cameraButtonText) TextView cameraButtonText; - @BindView(R.id.galeryButtonText) TextView galeryButtonText; + @BindView(R.id.galleryButtonText) TextView galleryButtonText; + @BindView(R.id.directionsButtonText) TextView directionsButtonText; @BindView(R.id.iconOverflowText) TextView iconOverflowText; @@ -41,8 +52,20 @@ class PlaceRenderer extends Renderer { private static ArrayList openedItems; private Place place; + private Fragment fragment; + private ContributionController controller; - PlaceRenderer(){ + + @Inject @Named("prefs") SharedPreferences prefs; + @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; + + public PlaceRenderer(){ + openedItems = new ArrayList<>(); + } + + public PlaceRenderer(Fragment fragment, ContributionController controller) { + this.fragment = fragment; + this.controller = controller; openedItems = new ArrayList<>(); } @@ -81,7 +104,28 @@ class PlaceRenderer extends Renderer { } }); - //TODO: Set onClickListeners for camera and gallery in list here + cameraButton.setOnClickListener(view2 -> { + Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); + DirectUpload directUpload = new DirectUpload(fragment, controller); + storeSharedPrefs(); + directUpload.initiateCameraUpload(); + }); + + galleryButton.setOnClickListener(view3 -> { + Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); + DirectUpload directUpload = new DirectUpload(fragment, controller); + storeSharedPrefs(); + directUpload.initiateGalleryUpload(); + }); + } + + private void storeSharedPrefs() { + SharedPreferences.Editor editor = directPrefs.edit(); + Timber.d("directPrefs stored"); + editor.putString("Title", place.getName()); + editor.putString("Desc", place.getLongDescription()); + editor.apply(); + } private void closeLayout(LinearLayout buttonLayout){ @@ -94,9 +138,11 @@ class PlaceRenderer extends Renderer { @Override public void render() { + place = getContent(); tvName.setText(place.name); - String descriptionText = place.getLabel().getText(); + String descriptionText = place.getLongDescription(); + if (descriptionText.equals("?")) { descriptionText = getContext().getString(R.string.no_description_found); } @@ -114,6 +160,7 @@ class PlaceRenderer extends Renderer { iconOverflow.setVisibility(showMenu() ? View.VISIBLE : View.GONE); iconOverflow.setOnClickListener(v -> popupMenuListener()); + } private void popupMenuListener() { 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..cdb9e97d3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -0,0 +1,90 @@ +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.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; + +import com.pedrogomez.renderers.RVRendererAdapter; + +import java.util.Collections; +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.setLayoutManager(new LinearLayoutManager(this)); + DividerItemDecoration itemDecor = new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL); + recyclerView.addItemDecoration(itemDecor); + addNotifications(); + } + + @SuppressLint("CheckResult") + private void addNotifications() { + Timber.d("Add notifications"); + + Observable.fromCallable(() -> controller.getNotifications()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(notificationList -> { + Collections.reverse(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/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 33e42e4be..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 @@ -25,11 +25,11 @@ import java.io.File; import javax.inject.Inject; import javax.inject.Named; -import dagger.android.AndroidInjection; 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 { @@ -40,8 +40,11 @@ public class SettingsFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { - AndroidInjection.inject(this); super.onCreate(savedInstanceState); + ApplicationlessInjection + .getInstance(getActivity().getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); // Load the preferences from an XML resource addPreferencesFromResource(R.xml.preferences); @@ -67,7 +70,12 @@ public class SettingsFragment extends PreferenceFragment { uploadLimit.setText(uploads + ""); uploadLimit.setSummary(uploads + ""); uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> { - int value = Integer.parseInt(newValue.toString()); + 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()) @@ -81,9 +89,9 @@ 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; 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 f08496914..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 @@ -4,10 +4,10 @@ import android.content.Intent; import android.os.Bundle; import android.preference.PreferenceManager; -import dagger.android.support.DaggerAppCompatActivity; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity; -public abstract class BaseActivity extends DaggerAppCompatActivity { +public abstract class BaseActivity extends CommonsDaggerAppCompatActivity { boolean currentTheme; @Override 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 60ea325e4..fa0e43957 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 @@ -5,6 +5,7 @@ import android.accounts.AccountManager; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; import android.support.v4.widget.DrawerLayout; @@ -27,6 +28,7 @@ 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; @@ -118,8 +120,9 @@ public abstract class NavigationBaseActivity extends BaseActivity return true; case R.id.action_feedback: drawerLayout.closeDrawer(navigationView); - Intent feedbackIntent = new Intent(Intent.ACTION_SEND); + Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO); feedbackIntent.setType("message/rfc822"); + feedbackIntent.setData(Uri.parse("mailto:")); feedbackIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{CommonsApplication.FEEDBACK_EMAIL}); feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, @@ -143,6 +146,10 @@ 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; 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 a04f7f8c8..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,7 +1,7 @@ package fr.free.nrw.commons.ui.widget; -/** - * Created by mikel on 07/08/2017. +/* + *Created by mikel on 07/08/2017. */ import android.content.Context; @@ -20,20 +20,22 @@ 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 + * @param attrs the set of attributes for the view */ public CompatTextView(Context context, AttributeSet attrs) { super(context, attrs); @@ -42,6 +44,7 @@ public class CompatTextView extends AppCompatTextView { /** * Constructs a new instance of CompatTextView + * * @param context * @param attrs * @param defStyleAttr @@ -53,6 +56,7 @@ public class CompatTextView extends AppCompatTextView { /** * initializes the view + * * @param attrs the attribute set of the view, which can be null */ private void init(@Nullable AttributeSet attrs) { 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 bf7742843..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 @@ -19,7 +19,7 @@ public abstract class OverlayDialog extends DialogFragment { /** * creates a DialogFragment with the correct style and theme - * @param savedInstanceState + * @param savedInstanceState bundle re-constructed from a previous saved state */ @Override public void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java new file mode 100644 index 000000000..b383601ec --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java @@ -0,0 +1,59 @@ +package fr.free.nrw.commons.upload; + +import android.content.Context; +import android.graphics.BitmapRegionDecoder; +import android.net.Uri; +import android.os.AsyncTask; + +import java.io.IOException; + +import fr.free.nrw.commons.utils.ImageUtils; +import timber.log.Timber; + +/** + * Created by bluesir9 on 16/9/17. + * + *

Responsible for checking if the picture that the user is trying to upload is useful or not. Will attempt to filter + * away completely black,fuzzy/blurry pictures(for now). + * + *

todo: Detect selfies? + */ + +public class DetectUnwantedPicturesAsync extends AsyncTask { + + interface Callback { + void onResult(ImageUtils.Result result); + } + + private final Callback callback; + private final String imageMediaFilePath; + + DetectUnwantedPicturesAsync(String imageMediaFilePath, Callback callback) { + this.callback = callback; + this.imageMediaFilePath = imageMediaFilePath; + } + + @Override + protected ImageUtils.Result doInBackground(Void... voids) { + try { + Timber.d("FilePath: " + imageMediaFilePath); + if (imageMediaFilePath == null) { + return ImageUtils.Result.IMAGE_OK; + } + + BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(imageMediaFilePath,false); + + return ImageUtils.checkIfImageIsTooDark(decoder); + } catch (IOException ioe) { + Timber.e(ioe, "IO Exception"); + return ImageUtils.Result.IMAGE_OK; + } + } + + @Override + protected void onPostExecute(ImageUtils.Result result) { + super.onPostExecute(result); + //callback to UI so that it can take necessary decision based on the result obtained + callback.onResult(result); + } +} 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 2cf18e7d7..13056ad4b 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 @@ -3,20 +3,25 @@ package fr.free.nrw.commons.upload; import android.annotation.SuppressLint; import android.content.ContentUris; import android.content.Context; +import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.preference.PreferenceManager; import android.provider.DocumentsContract; import android.provider.MediaStore; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; +import java.util.Date; import timber.log.Timber; @@ -28,7 +33,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 @@ -36,6 +41,7 @@ public class FileUtils { @Nullable public static String getPath(Context context, Uri uri) { + String returnPath = null; final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; // DocumentProvider @@ -47,31 +53,34 @@ public class FileUtils { final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { - return Environment.getExternalStorageDirectory() + "/" + split[1]; + returnPath = 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)) { + returnPath = getDataColumn(context, contentUri, null, null); + } 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=?"; @@ -79,16 +88,55 @@ public class FileUtils { split[1] }; - return getDataColumn(context, contentUri, selection, selectionArgs); + returnPath = getDataColumn(context, contentUri, selection, selectionArgs); } } // MediaStore (and general) else if ("content".equalsIgnoreCase(uri.getScheme())) { - return getDataColumn(context, uri, null, null); + returnPath = getDataColumn(context, uri, null, null); } // File else if ("file".equalsIgnoreCase(uri.getScheme())) { - return uri.getPath(); + returnPath = uri.getPath(); + } + + if(returnPath == null) { + //fetching path may fail depending on the source URI and all hope is lost + //so we will create and use a copy of the file, which seems to work + String copyPath = null; + try { + ParcelFileDescriptor descriptor + = context.getContentResolver().openFileDescriptor(uri, "r"); + if (descriptor != null) { + + SharedPreferences sharedPref = PreferenceManager + .getDefaultSharedPreferences(context); + boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true); + if (useExtStorage) { + copyPath = Environment.getExternalStorageDirectory().toString() + + "/CommonsApp/" + new Date().getTime() + ".jpg"; + File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); + newFile.mkdir(); + FileUtils.copy( + descriptor.getFileDescriptor(), + copyPath); + Timber.d("Filepath (copied): %s", copyPath); + return copyPath; + } + copyPath = context.getCacheDir().getAbsolutePath() + + "/" + new Date().getTime() + ".jpg"; + FileUtils.copy( + descriptor.getFileDescriptor(), + copyPath); + Timber.d("Filepath (copied): %s", copyPath); + return copyPath; + } + } catch (IOException e) { + Timber.w(e, "Error in file " + copyPath); + return null; + } + } else { + return returnPath; } return null; @@ -109,7 +157,7 @@ public class FileUtils { String[] selectionArgs) { Cursor cursor = null; - final String column = "_data"; + final String column = MediaStore.Images.ImageColumns.DATA; final String[] projection = { column }; @@ -163,7 +211,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 */ @@ -176,7 +225,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 404177032..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 @@ -113,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) { @@ -150,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; 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 8858a11f5..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,7 +2,6 @@ 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; @@ -12,7 +11,9 @@ import android.database.DataSetObserver; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.ParcelFileDescriptor; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentManager; import android.support.v4.content.ContextCompat; @@ -22,6 +23,7 @@ import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.Toast; +import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.List; @@ -40,6 +42,7 @@ 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.MediaWikiApi; import timber.log.Timber; @@ -51,10 +54,17 @@ public class MultipleShareActivity extends AuthenticatedActivity MultipleUploadListFragment.OnMultipleUploadInitiatedHandler, OnCategoriesSaveHandler { - @Inject MediaWikiApi mwApi; - @Inject SessionManager sessionManager; - @Inject UploadController uploadController; - @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject + MediaWikiApi mwApi; + @Inject + SessionManager sessionManager; + @Inject + UploadController uploadController; + @Inject + ModifierSequenceDao modifierSequenceDao; + @Inject + @Named("default_preferences") + SharedPreferences prefs; private ArrayList photosList = null; @@ -62,6 +72,8 @@ public class MultipleShareActivity extends AuthenticatedActivity private MediaDetailPagerFragment mediaDetails; private CategorizationFragment categorizationFragment; + private boolean locationPermitted = false; + @Override public Media getMediaAtPosition(int i) { return photosList.get(i); @@ -165,20 +177,18 @@ public class MultipleShareActivity extends AuthenticatedActivity @Override public void onCategoriesSave(List categories) { if (categories.size() > 0) { - ContentProviderClient client = getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY); 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(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default! + ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, true); // Enable sync by default! finish(); } @@ -208,6 +218,14 @@ public class MultipleShareActivity extends AuthenticatedActivity getSupportFragmentManager().addOnBackStackChangedListener(this); requestAuthToken(); + + //TODO: 15/10/17 should location permission be explicitly requested if not provided? + //check if location permission is enabled + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + locationPermitted = true; + } + } } @Override @@ -241,7 +259,7 @@ public class MultipleShareActivity extends AuthenticatedActivity 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); @@ -253,6 +271,11 @@ public class MultipleShareActivity extends AuthenticatedActivity up.setTag("sequence", i); up.setSource(Contribution.SOURCE_EXTERNAL); up.setMultiple(true); + String imageGpsCoordinates = extractImageGpsData(uri); + if (imageGpsCoordinates != null) { + Timber.d("GPS data for image found!"); + up.setDecimalCoords(imageGpsCoordinates); + } photosList.add(up); } } @@ -279,7 +302,49 @@ public class MultipleShareActivity extends AuthenticatedActivity @Override public void onBackStackChanged() { - getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible()) ; + getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible()); } + /** + * Will attempt to extract the gps coordinates using exif data or by using the current + * location if available for the image who's imageUri has been provided. + * @param imageUri The uri of the image who's GPS coordinates data we wish to extract + * @return GPS coordinates as a String as is returned by {@link GPSExtractor} + */ + @Nullable + private String extractImageGpsData(Uri imageUri) { + Timber.d("Entering extractImagesGpsData"); + + if (imageUri == null) { + //now why would you do that??? + return null; + } + + GPSExtractor gpsExtractor = null; + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r"); + if (fd != null) { + gpsExtractor = new GPSExtractor(fd.getFileDescriptor(),this,prefs); + } + } else { + String filePath = FileUtils.getPath(this,imageUri); + if (filePath != null) { + gpsExtractor = new GPSExtractor(filePath,this,prefs); + } + } + + if (gpsExtractor != null) { + //get image coordinates from exif data or user location + return gpsExtractor.getCoords(locationPermitted); + } + + } catch (FileNotFoundException fnfe) { + Timber.w(fnfe); + return null; + } + + return null; + } } \ No newline at end of file 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 d20d8c1eb..5b39b92f7 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 @@ -1,14 +1,17 @@ package fr.free.nrw.commons.upload; +import android.app.Activity; import android.content.Context; import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.support.graphics.drawable.VectorDrawableCompat; +import android.support.v4.app.Fragment; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.DisplayMetrics; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -27,12 +30,12 @@ import android.widget.TextView; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.view.SimpleDraweeView; -import dagger.android.support.DaggerFragment; +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; -public class MultipleUploadListFragment extends DaggerFragment { +public class MultipleUploadListFragment extends Fragment { public interface OnMultipleUploadInitiatedHandler { void OnMultipleUploadInitiated(); @@ -56,6 +59,12 @@ public class MultipleUploadListFragment extends DaggerFragment { private RelativeLayout overlay; } + @Override + public void onAttach(Context context) { + AndroidSupportInjection.inject(this); + super.onAttach(context); + } + private class PhotoDisplayAdapter extends BaseAdapter { @Override @@ -170,9 +179,21 @@ public class MultipleUploadListFragment extends DaggerFragment { photosGrid.setColumnWidth(photoSize.x); baseTitle.addTextChangedListener(textWatcher); + + baseTitle.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + hideKeyboard(v); + } + }); + return view; } + public void hideKeyboard(View view) { + InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + @Override public void onDestroyView() { baseTitle.removeTextChangedListener(textWatcher); 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 a9803acb1..ed02443c8 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 @@ -16,7 +16,9 @@ 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.support.v7.app.AlertDialog; import android.view.MenuItem; import android.view.View; import android.widget.TextView; @@ -46,10 +48,16 @@ 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.contributions.ContributionsActivity; 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; +<<<<<<< HEAD +======= +import fr.free.nrw.commons.utils.ImageUtils; +>>>>>>> directNearbyUploadsNewLocal import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; @@ -60,10 +68,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; @@ -71,11 +79,19 @@ public class ShareActivity private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4; private CategorizationFragment categorizationFragment; - @Inject MediaWikiApi mwApi; - @Inject CacheController cacheController; - @Inject SessionManager sessionManager; - @Inject UploadController uploadController; - @Inject @Named("default_preferences") SharedPreferences prefs; + @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; @@ -87,6 +103,7 @@ public class ShareActivity private boolean cacheFound; private GPSExtractor imageObj; + private GPSExtractor tempImageObj; private String decimalCoords; private boolean useNewPermissions = false; @@ -97,9 +114,14 @@ public class ShareActivity private String description; private Snackbar snackbar; private boolean duplicateCheckPassed = false; +<<<<<<< HEAD private boolean isNearbyUpload = false; +======= + private boolean haveCheckedForOtherImages = false; + private boolean isNearbyUpload = false; +>>>>>>> directNearbyUploadsNewLocal /** * Called when user taps the submit button. */ @@ -167,13 +189,12 @@ 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(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, true); // Enable sync by default! + ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, true); // Enable sync by default! finish(); } @@ -221,7 +242,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); @@ -287,7 +308,7 @@ public class ShareActivity REQUEST_PERM_ON_CREATE_LOCATION); } } - performPreuploadProcessingOfFile(); + performPreUploadProcessingOfFile(); SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView"); @@ -311,7 +332,7 @@ public class ShareActivity && grantResults[0] == PackageManager.PERMISSION_GRANTED) { backgroundImageView.setImageURI(mediaUri); storagePermitted = true; - performPreuploadProcessingOfFile(); + performPreUploadProcessingOfFile(); } return; } @@ -319,7 +340,7 @@ public class ShareActivity if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { locationPermitted = true; - performPreuploadProcessingOfFile(); + performPreUploadProcessingOfFile(); } return; } @@ -328,12 +349,12 @@ public class ShareActivity && grantResults[0] == PackageManager.PERMISSION_GRANTED) { backgroundImageView.setImageURI(mediaUri); storagePermitted = true; - performPreuploadProcessingOfFile(); + performPreUploadProcessingOfFile(); } if (grantResults.length >= 2 && grantResults[1] == PackageManager.PERMISSION_GRANTED) { locationPermitted = true; - performPreuploadProcessingOfFile(); + performPreUploadProcessingOfFile(); } return; } @@ -344,7 +365,7 @@ public class ShareActivity && grantResults[0] == PackageManager.PERMISSION_GRANTED) { //It is OK to call this at both (1) and (4) because if perm had been granted at //snackbar, user should not be prompted at submit button - performPreuploadProcessingOfFile(); + performPreUploadProcessingOfFile(); //Uploading only begins if storage permission granted from arrow icon uploadBegins(); @@ -355,7 +376,7 @@ public class ShareActivity } } - private void performPreuploadProcessingOfFile() { + private void performPreUploadProcessingOfFile() { if (!useNewPermissions || storagePermitted) { if (!duplicateCheckPassed) { //Test SHA1 of image to see if it matches SHA1 of a file on Commons @@ -370,7 +391,17 @@ public class ShareActivity Timber.d("%s duplicate check: %s", mediaUri.toString(), result); duplicateCheckPassed = (result == DUPLICATE_PROCEED || result == NO_DUPLICATE); - }, mwApi); + /* + TODO: 16/9/17 should we run DetectUnwantedPicturesAsync if DUPLICATE_PROCEED is returned? Since that means + we are processing images that are already on server???... + */ + + if (duplicateCheckPassed) { + //image can be uploaded, so now check if its a useless picture or not + performUnwantedPictureDetectionProcess(); + } + + },mwApi); fileAsyncTask.execute(); } catch (IOException e) { Timber.d(e, "IO Exception: "); @@ -384,6 +415,37 @@ public class ShareActivity } } + private void performUnwantedPictureDetectionProcess() { + String imageMediaFilePath = FileUtils.getPath(this,mediaUri); + DetectUnwantedPicturesAsync detectUnwantedPicturesAsync = new DetectUnwantedPicturesAsync(imageMediaFilePath, result -> { + + if (result != ImageUtils.Result.IMAGE_OK) { + //show appropriate error message + String errorMessage = result == ImageUtils.Result.IMAGE_DARK ? getString(R.string.upload_image_too_dark) : getString(R.string.upload_image_blurry); + AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(this); + errorDialogBuilder.setMessage(errorMessage); + errorDialogBuilder.setTitle(getString(R.string.warning)); + errorDialogBuilder.setPositiveButton(getString(R.string.no), (dialogInterface, i) -> { + //user does not wish to upload the picture, take them back to ContributionsActivity + Intent intent = new Intent(ShareActivity.this, ContributionsActivity.class); + dialogInterface.dismiss(); + startActivity(intent); + }); + errorDialogBuilder.setNegativeButton(getString(R.string.yes), (dialogInterface, i) -> { + //user wishes to go ahead with the upload of this picture, just dismiss this dialog + dialogInterface.dismiss(); + }); + + AlertDialog errorDialog = errorDialogBuilder.create(); + if (!isFinishing()) { + errorDialog.show(); + } + } + }); + + detectUnwantedPicturesAsync.execute(); + } + private Snackbar requestPermissionUsingSnackBar(String rationale, final String[] perms, final int code) { @@ -461,13 +523,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. @@ -475,6 +617,7 @@ 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) { @@ -498,7 +641,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 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 24d09ccd1..17ba61714 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,6 +1,10 @@ package fr.free.nrw.commons.upload; import android.annotation.SuppressLint; +<<<<<<< HEAD +======= +import android.app.Activity; +>>>>>>> directNearbyUploadsNewLocal import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -12,6 +16,7 @@ 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; @@ -37,16 +42,16 @@ import butterknife.ButterKnife; import butterknife.OnClick; import butterknife.OnItemSelected; import butterknife.OnTouch; -import dagger.android.support.DaggerFragment; 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 DaggerFragment { +public class SingleUploadFragment extends CommonsDaggerSupportFragment { @BindView(R.id.titleEdit) EditText titleEdit; @BindView(R.id.descEdit) EditText descEdit; @@ -147,11 +152,29 @@ public class SingleUploadFragment extends DaggerFragment { 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); 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 1d080e78f..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 @@ -6,6 +6,7 @@ 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; @@ -36,6 +37,9 @@ public class UploadController { void onUploadStarted(Contribution contribution); } + /** + * Constructs a new UploadController. + */ public UploadController(SessionManager sessionManager, Context context, SharedPreferences sharedPreferences) { this.sessionManager = sessionManager; this.context = context; @@ -46,17 +50,20 @@ public class UploadController { 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(context, UploadService.class); uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE); @@ -64,12 +71,26 @@ public class UploadController { context.bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE); } + /** + * Disconnects the upload service. + */ public void cleanup() { 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; @@ -85,6 +106,12 @@ 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) { //Set creator, desc, and license if (TextUtils.isEmpty(contribution.getCreator())) { @@ -110,15 +137,17 @@ public class UploadController { ContentResolver contentResolver = context.getContentResolver(); try { if (contribution.getDataLength() <= 0) { - length = contentResolver - .openAssetFileDescriptor(contribution.getLocalUri(), "r") - .getLength(); - if (length == -1) { - // Let us find out the long way! - length = countBytes(contentResolver - .openInputStream(contribution.getLocalUri())); + 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: "); @@ -128,7 +157,7 @@ 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("*")) { @@ -173,6 +202,13 @@ public class UploadController { } + /** + * 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); 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 8013c256c..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,7 +4,6 @@ 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; @@ -31,6 +30,7 @@ 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; @@ -51,9 +51,9 @@ public class UploadService extends HandlerService { @Inject MediaWikiApi mwApi; @Inject SessionManager sessionManager; @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject ContributionDao contributionDao; private NotificationManager notificationManager; - private ContentProviderClient contributionsProviderClient; private NotificationCompat.Builder curProgressNotification; private int toUpload; @@ -105,7 +105,7 @@ public class UploadService extends HandlerService { startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build()); contribution.setTransferred(transferred); - contribution.save(); + contributionDao.save(contribution); } } @@ -113,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); } @@ -122,7 +121,6 @@ public class UploadService extends HandlerService { super.onCreate(); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - contributionsProviderClient = this.getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY); } @Override @@ -144,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)); @@ -165,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); @@ -261,7 +257,7 @@ public class UploadService extends HandlerService { contribution.setImageUrl(uploadResult.getImageUrl()); contribution.setState(Contribution.STATE_COMPLETED); contribution.setDateUploaded(uploadResult.getDateUploaded()); - contribution.save(); + contributionDao.save(contribution); } } catch (IOException e) { Timber.d("I have a network fuckup"); @@ -273,7 +269,7 @@ public class UploadService extends HandlerService { toUpload--; if (toUpload == 0) { // Sync modifications right after all uplaods are processed - ContentResolver.requestSync(sessionManager.getCurrentAccount(), ModificationsContentProvider.AUTHORITY, new Bundle()); + ContentResolver.requestSync(sessionManager.getCurrentAccount(), ModificationsContentProvider.MODIFICATIONS_AUTHORITY, new Bundle()); stopForeground(true); } } @@ -292,7 +288,7 @@ 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 { 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 15f18ee5e..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 4191f9d6f..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 @@ -4,31 +4,34 @@ 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) { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java new file mode 100644 index 000000000..6627f2886 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java @@ -0,0 +1,135 @@ +package fr.free.nrw.commons.utils; + +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Color; +import android.graphics.Rect; + +import timber.log.Timber; + +/** + * Created by bluesir9 on 3/10/17. + */ + +public class ImageUtils { + //atleast 50% of the image in question should be considered dark for the entire image to be dark + private static final double MINIMUM_DARKNESS_FACTOR = 0.50; + //atleast 50% of the image in question should be considered blurry for the entire image to be blurry + private static final double MINIMUM_BLURRYNESS_FACTOR = 0.50; + private static final int LAPLACIAN_VARIANCE_THRESHOLD = 70; + + public enum Result { + IMAGE_DARK, + IMAGE_OK + } + + /** + * BitmapRegionDecoder allows us to process a large bitmap by breaking it down into smaller rectangles. The rectangles + * are obtained by setting an initial width, height and start position of the rectangle as a factor of the width and + * height of the original bitmap and then manipulating the width, height and position to loop over the entire original + * bitmap. Each individual rectangle is independently processed to check if its too dark. Based on + * the factor of "bright enough" individual rectangles amongst the total rectangles into which the image + * was divided, we will declare the image as wanted/unwanted + * + * @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process + * @return Result.IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null + * Result.IMAGE_DARK if image is too dark + */ + public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) { + if (bitmapRegionDecoder == null) { + Timber.e("Expected bitmapRegionDecoder was null"); + return Result.IMAGE_OK; + } + + int loadImageHeight = bitmapRegionDecoder.getHeight(); + int loadImageWidth = bitmapRegionDecoder.getWidth(); + + int checkImageTopPosition = 0; + int checkImageBottomPosition = loadImageHeight / 10; + int checkImageLeftPosition = 0; + int checkImageRightPosition = loadImageWidth / 10; + + int totalDividedRectangles = 0; + int numberOfDarkRectangles = 0; + + while ((checkImageRightPosition <= loadImageWidth) && (checkImageLeftPosition < checkImageRightPosition)) { + while ((checkImageBottomPosition <= loadImageHeight) && (checkImageTopPosition < checkImageBottomPosition)) { + Timber.d("left: " + checkImageLeftPosition + " right: " + checkImageRightPosition + " top: " + checkImageTopPosition + " bottom: " + checkImageBottomPosition); + + Rect rect = new Rect(checkImageLeftPosition,checkImageTopPosition,checkImageRightPosition,checkImageBottomPosition); + totalDividedRectangles++; + + Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null); + + if (checkIfImageIsDark(processBitmap)) { + numberOfDarkRectangles++; + } + + checkImageTopPosition = checkImageBottomPosition; + checkImageBottomPosition += (checkImageBottomPosition < (loadImageHeight - checkImageBottomPosition)) ? checkImageBottomPosition : (loadImageHeight - checkImageBottomPosition); + } + + checkImageTopPosition = 0; //reset to start + checkImageBottomPosition = loadImageHeight / 10; //reset to start + checkImageLeftPosition = checkImageRightPosition; + checkImageRightPosition += (checkImageRightPosition < (loadImageWidth - checkImageRightPosition)) ? checkImageRightPosition : (loadImageWidth - checkImageRightPosition); + } + + Timber.d("dark rectangles count = " + numberOfDarkRectangles + ", total rectangles count = " + totalDividedRectangles); + + if (numberOfDarkRectangles > totalDividedRectangles * MINIMUM_DARKNESS_FACTOR) { + return Result.IMAGE_DARK; + } + + return Result.IMAGE_OK; + } + + /** + * Pulls the pixels into an array and then runs through it while checking the brightness of each pixel. + * The calculation of brightness of each pixel is done by extracting the RGB constituents of the pixel + * and then applying the formula to calculate its "Luminance". If this brightness value is less than + * 50 then the pixel is considered to be dark. Based on the MINIMUM_DARKNESS_FACTOR if enough pixels + * are dark then the entire bitmap is considered to be dark. + * + *

For more information on this brightness/darkness calculation technique refer the accepted answer + * on this -> https://stackoverflow.com/questions/35914461/how-to-detect-dark-photos-in-android/35914745 + * SO question and follow the trail. + * + * @param bitmap The bitmap that needs to be checked. + * @return true if bitmap is dark or null, false if bitmap is bright + */ + private static boolean checkIfImageIsDark(Bitmap bitmap) { + if (bitmap == null) { + Timber.e("Expected bitmap was null"); + return true; + } + + int bitmapWidth = bitmap.getWidth(); + int bitmapHeight = bitmap.getHeight(); + + int allPixelsCount = bitmapWidth * bitmapHeight; + int[] bitmapPixels = new int[allPixelsCount]; + + bitmap.getPixels(bitmapPixels,0,bitmapWidth,0,0,bitmapWidth,bitmapHeight); + boolean isImageDark = false; + int darkPixelsCount = 0; + + for (int pixel : bitmapPixels) { + int r = Color.red(pixel); + int g = Color.green(pixel); + int b = Color.blue(pixel); + + int brightness = (int) (0.2126 * r + 0.7152 * g + 0.0722 * b); + if (brightness < 50) { + //pixel is dark + darkPixelsCount++; + if (darkPixelsCount > allPixelsCount * MINIMUM_DARKNESS_FACTOR) { + isImageDark = true; + break; + } + } + } + + return isImageDark; + } +} 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 index 947c70eb6..f2a02398f 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java @@ -7,11 +7,6 @@ import android.widget.Toast; public class ViewUtil { public static void showLongToast(final Context context, @StringRes final int stringResId) { - ExecutorUtils.uiExecutor().execute(new Runnable() { - @Override - public void run() { - Toast.makeText(context, context.getString(stringResId), Toast.LENGTH_LONG).show(); - } - }); + ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResId), Toast.LENGTH_LONG).show()); } } 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/layout-land/activity_login.xml b/app/src/main/res/layout-land/activity_login.xml index 7265ba671..a87084f66 100644 --- a/app/src/main/res/layout-land/activity_login.xml +++ b/app/src/main/res/layout-land/activity_login.xml @@ -8,17 +8,17 @@ android:layout_width="400sp" android:layout_height="wrap_content" android:layout_gravity="center" - android:layout_marginTop="8dp"> + android:layout_marginTop="@dimen/small_gap"> @@ -34,17 +34,32 @@ android:layout_height="wrap_content" android:background="@color/primaryColor" android:gravity="center" - android:paddingBottom="32dp" - android:paddingTop="32dp" + android:paddingBottom="@dimen/large_gap" + android:paddingTop="@dimen/large_gap" android:text="@string/login_to_your_account" android:textColor="@android:color/white" - android:textSize="24sp" /> + android:textSize="@dimen/heading_text_size" /> + + @@ -52,12 +67,13 @@ android:id="@+id/error_message" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginEnd="16dp" - android:layout_marginLeft="16dp" - android:layout_marginRight="16dp" - android:layout_marginStart="16dp" - android:paddingBottom="8dp" - android:paddingTop="8dp" + android:layout_marginEnd="@dimen/standard_gap" + android:layout_marginLeft="@dimen/standard_gap" + android:layout_marginRight="@dimen/standard_gap" + android:layout_marginStart="@dimen/standard_gap" + android:gravity="center" + android:paddingBottom="@dimen/small_gap" + android:paddingTop="@dimen/small_gap" android:textColor="@color/secondaryDarkColor" tools:text="Check your password, something doesnt look right" /> @@ -67,12 +83,12 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/error_message_container" - android:layout_marginBottom="16dp" - android:layout_marginEnd="16dp" - android:layout_marginLeft="16dp" - android:layout_marginRight="16dp" - android:layout_marginStart="16dp" - android:layout_marginTop="16dp"> + android:layout_marginBottom="@dimen/standard_gap" + android:layout_marginEnd="@dimen/standard_gap" + android:layout_marginLeft="@dimen/standard_gap" + android:layout_marginRight="@dimen/standard_gap" + android:layout_marginStart="@dimen/standard_gap" + android:layout_marginTop="@dimen/standard_gap"> @@ -141,57 +157,46 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/two_factor_container" - android:layout_marginBottom="16dp" - android:layout_marginEnd="16dp" - android:layout_marginLeft="16dp" - android:layout_marginRight="16dp" - android:layout_marginStart="16dp"> + android:layout_marginBottom="@dimen/standard_gap" + android:layout_marginEnd="@dimen/standard_gap" + android:layout_marginLeft="@dimen/standard_gap" + android:layout_marginRight="@dimen/standard_gap" + android:layout_marginStart="@dimen/standard_gap">