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..691562b04 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,39 @@ +_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. + +**Would you like to work on the issue?** + +Please let us know whether you want to fix the issue by yourself. If not, anyone can get the issue assigned to them. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..34078f07e --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Description + +Fixes #{GitHub issue number} + +{Describe the changes made and why they were made.} + +## Tests performed + +Tested on {API level & name of device/emulator}, with {build variant, e.g. ProdDebug}. + +{Please test your PR at least once before submitting.} + +## 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 eab310df4..ac181a36c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,7 +18,7 @@ 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 } @@ -26,6 +26,7 @@ dependencies { 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:$SUPPORT_LIB_VERSION" @@ -38,6 +39,10 @@ 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. + implementation 'com.android.support:multidex:1.0.3' + + testImplementation "org.robolectric:multidex:3.4.2" + 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' @@ -57,7 +62,7 @@ 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' @@ -73,6 +78,9 @@ dependencies { 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" + + implementation 'com.borjabravo:readmoretextview:2.1.0' + implementation 'com.android.support.constraint:constraint-layout:1.0.2' } android { @@ -83,18 +91,31 @@ 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 + } + + testOptions { + unitTests.all { + jvmArgs '-noverify' + } } 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 { @@ -121,6 +142,7 @@ android { buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\"" buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\"" + buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\"" dimension 'tier' } @@ -135,6 +157,7 @@ android { buildConfigField "String", "EVENTLOG_WIKI", "\"commonswiki\"" buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\"" + buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\"" dimension 'tier' } } diff --git a/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityTest.java b/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityTest.java index 5be309252..80caf0010 100644 --- a/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityTest.java +++ b/app/src/androidTest/java/fr/free/nrw/commons/SettingsActivityTest.java @@ -5,6 +5,7 @@ import android.preference.PreferenceManager; import android.support.test.espresso.Espresso; import android.support.test.espresso.action.ViewActions; import android.support.test.espresso.assertion.ViewAssertions; +import android.support.test.espresso.matcher.PreferenceMatchers; import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.filters.LargeTest; import android.support.test.rule.ActivityTestRule; @@ -61,7 +62,7 @@ public class SettingsActivityTest { @Test public void oneLicenseIsChecked() { // click "License" (the first item) - Espresso.onData(Matchers.anything()) + Espresso.onData(PreferenceMatchers.withKey("defaultLicense")) .inAdapterView(ViewMatchers.withId(android.R.id.list)) .atPosition(0) .perform(ViewActions.click()); @@ -74,7 +75,7 @@ public class SettingsActivityTest { @Test public void afterClickingCcby4ItWillStay() { // click "License" (the first item) - Espresso.onData(Matchers.anything()) + Espresso.onData(PreferenceMatchers.withKey("defaultLicense")) .inAdapterView(ViewMatchers.withId(android.R.id.list)) .atPosition(0) .perform(ViewActions.click()); @@ -85,7 +86,7 @@ public class SettingsActivityTest { ).perform(ViewActions.click()); // click "License" (the first item) - Espresso.onData(Matchers.anything()) + Espresso.onData(PreferenceMatchers.withKey("defaultLicense")) .inAdapterView(ViewMatchers.withId(android.R.id.list)) .atPosition(0) .perform(ViewActions.click()); @@ -96,4 +97,4 @@ public class SettingsActivityTest { ViewMatchers.withText(R.string.license_name_cc_by_four) )); } -} \ No newline at end of file +} diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java index 6ed1bba1d..ecf4c21f0 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java @@ -1,19 +1,48 @@ package fr.free.nrw.commons; +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; +import android.text.SpannableString; +import android.text.style.UnderlineSpan; +import android.util.Log; +import android.support.customtabs.CustomTabsIntent; +import android.support.v4.content.ContextCompat; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.Spinner; import android.widget.TextView; +import android.widget.Toast; import butterknife.BindView; import butterknife.ButterKnife; +import butterknife.OnClick; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.ui.widget.HtmlTextView; +import static android.widget.Toast.LENGTH_SHORT; + /** * Represents about screen of this app */ public class AboutActivity extends NavigationBaseActivity { @BindView(R.id.about_version) TextView versionText; @BindView(R.id.about_license) HtmlTextView aboutLicenseText; + @BindView(R.id.about_faq) TextView faqText; + + String language[] = { "Kazakh", "Afrikaans", "Arabic", "Bengali", "Asturianu", "azərbaycanca", "Bikol Central", + "Bulgarain", "বাংলা", "Bosanski", "Brezhoneg","català","کوردی", " čeština", " kaszëbsczi", "Cymraeg", "dansk", "Deutsch" + ,"Zazaki", "डोटेली","Ελληνικά","euskara","español","فارسی","suomi", "français" ,"Nordfriisk", "galego", "Hawaiʻi" + ,"हिन्दी","Hunsrik","עברית","hornjoserbsce","magyar","interlingua","Bahasa Indonesia", "íslenska","Italian","japanese", + "Basa Jawa", "ქართული", " ភាសាខ្មែរ","ಕನ್ನಡ", "한국어","къарачай-малкъар","Кыргызча", "latina", "Lëtzebuergesch", "lietuvių", + "latviešu", "Malagasy", "македонски"," മലയാളം","монгол","मराठी","Bahasa Melayu","Malti", "नेपाली", "norsk bokmål", + " Nederlands","occitan","ଓଡ଼ିଆ","ਪੰਜਾਬੀ","polsk","Piemontèis","پښتو","português","română","русский"," سنڌي", " සිංහල", + "slovenčina"," سرائیکی", "svenska", "தமிழ்", "ತುಳು"," తెలుగు"," ไทย", "Türkçe","українська", "اردو", "Tiếng Việt", + " მარგალური","ייִדיש",}; /** * This method helps in the creation About screen @@ -21,16 +50,95 @@ public class AboutActivity extends NavigationBaseActivity { * @param savedInstanceState Data bundle */ @Override + @SuppressLint("StringFormatInvalid") public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_about); ButterKnife.bind(this); - - String aboutText = getString(R.string.about_license, getString(R.string.trademarked_name)); + String aboutText = getString(R.string.about_license); aboutLicenseText.setHtmlText(aboutText); - + SpannableString content = new SpannableString(getString(R.string.about_faq)); + content.setSpan(new UnderlineSpan(), 0, content.length(), 0); + faqText.setText(content); versionText.setText(BuildConfig.VERSION_NAME); initDrawer(); } -} \ No newline at end of file + + @OnClick(R.id.facebook_launch_icon) + public void launchFacebook(View view) { + Intent intent; + try { + intent = new Intent(Intent.ACTION_VIEW, Uri.parse("fb://page/" + "1921335171459985")); + intent.setPackage("com.facebook.katana"); + startActivity(intent); + } catch (Exception e) { + Utils.handleWebUrl(this,Uri.parse("https://www.facebook.com/" + "1921335171459985")); + } + } + + @OnClick(R.id.github_launch_icon) + public void launchGithub(View view) { + Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons\\")); + } + + @OnClick(R.id.website_launch_icon) + public void launchWebsite(View view) { + Utils.handleWebUrl(this,Uri.parse("https://commons-app.github.io/\\")); + } + + @OnClick(R.id.about_rate_us) + public void launchRatings(View view){ + Utils.rateApp(this); + } + + @OnClick(R.id.about_credits) + public void launchCredits(View view) { + Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/blob/master/CREDITS/\\")); + } + + @OnClick(R.id.about_privacy_policy) + public void launchPrivacyPolicy(View view) { + Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\\")); + } + + + @OnClick(R.id.about_faq) + public void launchFrequentlyAskedQuesions(View view) { + Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Frequently-Asked-Questions\\")); + } + + @OnClick(R.id.about_translate) + public void launchTranslate(View view) { + final ArrayAdapter languageAdapter = new ArrayAdapter(AboutActivity.this, + android.R.layout.simple_spinner_item, language); + final Spinner spinner = new Spinner(AboutActivity.this); + spinner.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + spinner.setAdapter(languageAdapter); + spinner.setGravity(17); + + AlertDialog.Builder builder = new AlertDialog.Builder(AboutActivity.this); + builder.setView(spinner); + builder.setTitle(R.string.about_translate_title) + .setMessage(R.string.about_translate_message) + .setPositiveButton(R.string.about_translate_proceed, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String languageSelected = spinner.getSelectedItem().toString(); + TokensTranslations tokensTranslations = new TokensTranslations(); + tokensTranslations.initailize(); + String token = tokensTranslations.getTranslationToken(languageSelected); + Utils.handleWebUrl(AboutActivity.this,Uri.parse("https://translatewiki.net/w/i.php?title=Special:Translate&language="+token+"&group=commons-android-strings&filter=%21translated&action=translate ?")); + } + }); + builder.setNegativeButton(R.string.about_translate_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + finish(); + } + }); + builder.create().show(); + + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index b7e1a6039..5638db97e 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -1,10 +1,13 @@ package fr.free.nrw.commons; +import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; +import android.support.multidex.MultiDexApplication; import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.imagepipeline.core.ImagePipelineConfig; import com.facebook.stetho.Stetho; import com.squareup.leakcanary.LeakCanary; import com.squareup.leakcanary.RefWatcher; @@ -18,15 +21,11 @@ import java.io.File; import javax.inject.Inject; import javax.inject.Named; -import dagger.android.AndroidInjector; -import dagger.android.DaggerApplication; import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.category.CategoryDao; import fr.free.nrw.commons.contributions.ContributionDao; -import fr.free.nrw.commons.data.CategoryDao; import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.di.CommonsApplicationComponent; -import fr.free.nrw.commons.di.CommonsApplicationModule; -import fr.free.nrw.commons.di.DaggerCommonsApplicationComponent; +import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.utils.FileUtils; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -42,15 +41,16 @@ import timber.log.Timber; resDialogCommentPrompt = R.string.crash_dialog_comment_prompt, resDialogOkToast = R.string.crash_dialog_ok_toast ) -public class CommonsApplication extends DaggerApplication { +public class CommonsApplication extends MultiDexApplication { @Inject SessionManager sessionManager; @Inject DBOpenHelper dbOpenHelper; + @Inject @Named("default_preferences") SharedPreferences defaultPrefs; @Inject @Named("application_preferences") SharedPreferences applicationPrefs; @Inject @Named("prefs") SharedPreferences otherPrefs; - public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using Android Commons app"; + public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using [[COM:MOA|Commons Mobile App]]"; public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com"; @@ -58,9 +58,9 @@ public class CommonsApplication extends DaggerApplication { public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback"; - private CommonsApplicationComponent component; private RefWatcher refWatcher; + /** * Used to declare and initialize various components and dependencies */ @@ -68,7 +68,15 @@ public class CommonsApplication extends DaggerApplication { public void onCreate() { super.onCreate(); - Fresco.initialize(this); + ApplicationlessInjection + .getInstance(this) + .getCommonsApplicationComponent() + .inject(this); +// Set DownsampleEnabled to True to downsample the image in case it's heavy + ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this) + .setDownsampleEnabled(true) + .build(); + Fresco.initialize(this,config); if (setupLeakCanary() == RefWatcher.DISABLED) { return; } @@ -85,6 +93,7 @@ public class CommonsApplication extends DaggerApplication { System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0"); } + /** * Helps in setting up LeakCanary library * @return instance of LeakCanary @@ -107,28 +116,6 @@ public class CommonsApplication extends DaggerApplication { return application.refWatcher; } - /** - * Helps in injecting dependency library Dagger - * @return Dagger injector - */ - @Override - protected AndroidInjector 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 * @param context Application context @@ -152,9 +139,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(); 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 03fee8852..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; @@ -71,7 +72,11 @@ public class MediaWikiImageView extends SimpleDraweeView { * 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(), diff --git a/app/src/main/java/fr/free/nrw/commons/TokensTranslations.java b/app/src/main/java/fr/free/nrw/commons/TokensTranslations.java new file mode 100644 index 000000000..92278b6e9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/TokensTranslations.java @@ -0,0 +1,105 @@ +package fr.free.nrw.commons; + +import java.util.HashMap; + +/** + * Created by Dell on 3/16/2018. + */ + +public class TokensTranslations { + HashMap translationToken = new HashMap(); + + public void initailize() { + translationToken.put("Kazakh", "ab"); + translationToken.put("Afrikaans", "af"); + translationToken.put("Arabic", "ar"); + translationToken.put("Bengali", "as"); + translationToken.put("Asturianu", "ast"); + translationToken.put("azərbaycanca", "az"); + translationToken.put("Bikol Central", "bcl"); + translationToken.put("Bulgarain","bg"); + translationToken.put("বাংলা", "bn"); + translationToken.put("Brezhoneg", "br"); + translationToken.put("Bosanski", "bs"); + translationToken.put("català", "ca"); + translationToken.put("کوردی","ckb"); + translationToken.put("čeština", "cs"); + translationToken.put("kaszëbsczi", "csb"); + translationToken.put("Cymraeg", "cy"); + translationToken.put("dansk", "da"); + translationToken.put("Deutsch", "de"); + translationToken.put("Zazaki", "diq"); + translationToken.put("डोटेली","diq"); + translationToken.put("Ελληνικά","el"); + translationToken.put("euskara","eu"); + translationToken.put("español", "es"); + translationToken.put("فارسی","fa"); + translationToken.put("suomi", "fi"); + translationToken.put("føroyskt", "fo"); + translationToken.put("français", "fr"); + translationToken.put("Nordfriisk", "frr"); + translationToken.put("galego", "gr"); + translationToken.put("Hawaiʻi", "haw"); + translationToken.put("עברית","he"); + translationToken.put("हिन्दी","hi"); + translationToken.put("Hunsrik", "hrx"); + translationToken.put("hornjoserbsce", "hsb"); + translationToken.put("magyar","hu"); + translationToken.put("interlingua","ia"); + translationToken.put("Bahasa Indonesia", "id"); + translationToken.put("íslenska","is"); + translationToken.put("Italian","it"); + translationToken.put("japanese","ja"); + translationToken.put("Basa Jawa","jv"); + translationToken.put("ქართული", "ka"); + translationToken.put("Taqbaylit","kab"); + translationToken.put(" ភាសាខ្មែរ","km"); + translationToken.put("ಕನ್ನಡ", "kn"); + translationToken.put("한국어", "ko"); + translationToken.put("къарачай-малкъар","krc"); + translationToken.put("Кыргызча","ky"); + translationToken.put("latina","la"); + translationToken.put("Lëtzebuergesch","lb"); + translationToken.put("lietuvių", "lt"); + translationToken.put("latviešu","lv"); + translationToken.put("Malagasy","mg"); + translationToken.put("македонски", "mk"); + translationToken.put("മലയാളം","ml"); + translationToken.put("монгол","mn"); + translationToken.put("मराठी","mr"); + translationToken.put("Bahasa Melayu","ms"); + translationToken.put("Malti","mt"); + translationToken.put("norsk bokmål", "nb"); + translationToken.put("नेपाली","ne"); + translationToken.put("Nederlands","nl"); + translationToken.put("occitan","oc"); + translationToken.put("ଓଡ଼ିଆ","or"); + translationToken.put("ਪੰਜਾਬੀ","pa"); + translationToken.put("polsk", "pl"); + translationToken.put("Piemontèis","pms"); + translationToken.put("پښتو","ps"); + translationToken.put("português","pt"); + translationToken.put("română","ro"); + translationToken.put("русский","ru"); + translationToken.put(" سنڌي","sd"); + translationToken.put(" සිංහල","si"); + translationToken.put("slovenčina","sk"); + translationToken.put(" سرائیکی","skr"); + translationToken.put("Basa Sunda","su"); + translationToken.put("svenska","sv"); + translationToken.put("தமிழ்", "ta"); + translationToken.put("ತುಳು", "tcy"); + translationToken.put(" తెలుగు","te"); + translationToken.put(" ไทย","th"); + translationToken.put("Türkçe","tr"); + translationToken.put("українська","uk"); + translationToken.put("اردو","ur"); + translationToken.put("Tiếng Việt","vi"); + translationToken.put(" მარგალური", "xmf"); + translationToken.put("ייִדיש","yi"); + } + + public String getTranslationToken ( String language){ + return translationToken.get(language); + } +} 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..05873782c 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,13 @@ 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 android.widget.Toast; import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; @@ -19,6 +24,8 @@ import java.util.regex.Pattern; import fr.free.nrw.commons.settings.Prefs; import timber.log.Timber; +import static android.widget.Toast.LENGTH_SHORT; + public class Utils { /** @@ -65,7 +72,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 +166,32 @@ public class Utils { return stringBuilder.toString(); } + + public static void rateApp(Context context) { + final String appPackageName = BuildConfig.class.getPackage().getName(); + try { + context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + appPackageName))); + } + catch (android.content.ActivityNotFoundException anfe) { + context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + appPackageName))); + } + } + + public static void handleWebUrl(Context context,Uri url){ + Intent browserIntent = new Intent(Intent.ACTION_VIEW, url); + if (browserIntent.resolveActivity(context.getPackageManager()) == null) { + Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT); + toast.show(); + return; + } + + 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/WelcomePagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java index a346655cf..705de23da 100644 --- a/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java @@ -5,6 +5,7 @@ import android.support.v4.view.PagerAdapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import butterknife.ButterKnife; import butterknife.OnClick; @@ -54,10 +55,18 @@ public class WelcomePagerAdapter extends PagerAdapter { public Object instantiateItem(ViewGroup container, int position) { LayoutInflater inflater = LayoutInflater.from(container.getContext()); ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false); - - if (position == PAGE_FINAL) { + if( BuildConfig.FLAVOR == "beta"){ + TextView textView = (TextView) layout.findViewById(R.id.welcomeYesButton); + if( textView.getVisibility() != View.VISIBLE){ + textView.setVisibility(View.VISIBLE); + } ViewHolder holder = new ViewHolder(layout); layout.setTag(holder); + } else { + if (position == PAGE_FINAL) { + ViewHolder holder = new ViewHolder(layout); + layout.setTag(holder); + } } container.addView(layout); return layout; @@ -92,5 +101,6 @@ public class WelcomePagerAdapter extends PagerAdapter { callback.onYesClicked(); } } + } } 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..256c7e3b3 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,44 +1,63 @@ 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; +import android.net.Uri; 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 android.widget.Toast; + +import java.io.IOException; import javax.inject.Inject; import javax.inject.Named; import butterknife.BindView; import butterknife.ButterKnife; -import dagger.android.AndroidInjection; +import butterknife.OnClick; 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 fr.free.nrw.commons.ui.widget.HtmlTextView; +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,31 +76,77 @@ 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; + @BindView(R.id.forgotPassword) HtmlTextView forgotPasswordText; + ProgressDialog progressDialog; private AppCompatDelegate delegate; private LoginTextWatcher textWatcher = new LoginTextWatcher(); + private Boolean loginCurrentlyInProgress = false; + private static final String LOGING_IN = "logingIn"; + @Override public void onCreate(Bundle savedInstanceState) { 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()); + + forgotPasswordText.setOnClickListener(view -> forgotPassword()); + + if(BuildConfig.FLAVOR == "beta"){ + loginCredentials.setText(getString(R.string.login_credential)); + } else { + loginCredentials.setVisibility(View.GONE); + } } + private void forgotPassword() { + Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)); + } + + @OnClick(R.id.about_privacy_policy) + void onPrivacyPolicyClicked() { + Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\\")); + } + + 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 +182,111 @@ 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() { + loginCurrentlyInProgress = true; + 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 { + loginCurrentlyInProgress = false; + 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); + } } /** @@ -175,15 +337,28 @@ public class LoginActivity extends AccountAuthenticatorActivity { return getDelegate().getMenuInflater(); } - 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); + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(LOGING_IN, loginCurrentlyInProgress); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGING_IN, false); + if(loginCurrentlyInProgress){ + performLogin(); } } + public void askUserForTwoFactorAuth() { + progressDialog.dismiss(); + twoFactorContainer.setVisibility(VISIBLE); + twoFactorEdit.setVisibility(VISIBLE); + showMessageAndCancelDialog(R.string.login_failed_2fa_needed); + } + public void showMessageAndCancelDialog(@StringRes int resId) { showMessage(resId, R.color.secondaryDarkColor); progressDialog.cancel(); @@ -204,12 +379,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 +402,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 +424,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 335f7364c..a873136fe 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,23 @@ 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,12 +39,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.data.CategoryDao; +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; @@ -48,12 +52,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; @@ -70,12 +73,16 @@ public class CategorizationFragment extends DaggerFragment { @Inject MediaWikiApi mwApi; @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject @Named("prefs") SharedPreferences prefsPrefs; + @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; + @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 boolean hasDirectCategories = false; private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> { if (item.isSelected()) { @@ -106,6 +113,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) @@ -114,6 +130,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(); @@ -138,12 +166,6 @@ public class CategorizationFragment extends DaggerFragment { } } - @Override - public void onDestroy() { - super.onDestroy(); - databaseClient.release(); - } - @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -179,7 +201,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) { @@ -205,7 +226,7 @@ public class CategorizationFragment extends DaggerFragment { .observeOn(AndroidSchedulers.mainThread()) .subscribe( s -> categoriesAdapter.add(s), - Timber::e, + Timber::e, () -> { categoriesAdapter.notifyDataSetChanged(); categoriesSearchInProgress.setVisibility(View.GONE); @@ -240,9 +261,34 @@ public class CategorizationFragment extends DaggerFragment { } private Observable defaultCategories() { - return gpsCategories() - .concatWith(titleCategories()) - .concatWith(recentCategories()); + + Observable directCat = directCategories(); + if (hasDirectCategories) { + Timber.d("Image has direct Cat"); + return directCat + .concatWith(gpsCategories()) + .concatWith(titleCategories()) + .concatWith(recentCategories()); + } + else { + Timber.d("Image has no direct Cat"); + return gpsCategories() + .concatWith(titleCategories()) + .concatWith(recentCategories()); + } + } + + private Observable directCategories() { + String directCategory = directPrefs.getString("Category", ""); + List categoryList = new ArrayList<>(); + Timber.d("Direct category found: " + directCategory); + + if (!directCategory.equals("")) { + hasDirectCategories = true; + categoryList.add(directCategory); + Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList); + } + return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false)); } private Observable gpsCategories() { @@ -262,7 +308,7 @@ public class CategorizationFragment extends DaggerFragment { } private Observable recentCategories() { - return Observable.fromIterable(new CategoryDao(databaseClient).recentCategories(SEARCH_CATS_LIMIT)) + return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT)) .map(s -> new CategoryItem(s, false)); } @@ -308,12 +354,13 @@ 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) { - CategoryDao categoryDao = new CategoryDao(databaseClient); Category category = categoryDao.find(item.getName()); // Newly used category... @@ -361,4 +408,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/data/Category.java b/app/src/main/java/fr/free/nrw/commons/category/Category.java similarity index 97% rename from app/src/main/java/fr/free/nrw/commons/data/Category.java rename to app/src/main/java/fr/free/nrw/commons/category/Category.java index 9a32f3a7a..f2d83d2e5 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/Category.java +++ b/app/src/main/java/fr/free/nrw/commons/category/Category.java @@ -1,4 +1,4 @@ -package fr.free.nrw.commons.data; +package fr.free.nrw.commons.category; import android.net.Uri; 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 3384a984b..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.CategoryDao.Table.ALL_FIELDS; -import static fr.free.nrw.commons.data.CategoryDao.Table.COLUMN_ID; -import static fr.free.nrw.commons.data.CategoryDao.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/data/CategoryDao.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java similarity index 78% rename from app/src/main/java/fr/free/nrw/commons/data/CategoryDao.java rename to app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java index 8bae4a522..a5202046b 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/CategoryDao.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java @@ -1,4 +1,4 @@ -package fr.free.nrw.commons.data; +package fr.free.nrw.commons.category; import android.content.ContentProviderClient; import android.content.ContentValues; @@ -12,25 +12,31 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; -import fr.free.nrw.commons.category.CategoryContentProvider; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; public class CategoryDao { - private final ContentProviderClient client; + private final Provider clientProvider; - public CategoryDao(ContentProviderClient client) { - this.client = client; + @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(client.insert(CategoryContentProvider.BASE_URI, toContentValues(category))); + category.setContentUri(db.insert(CategoryContentProvider.BASE_URI, toContentValues(category))); } else { - client.update(category.getContentUri(), toContentValues(category), null, null); + db.update(category.getContentUri(), toContentValues(category), null, null); } } catch (RemoteException e) { throw new RuntimeException(e); + } finally { + db.release(); } } @@ -40,11 +46,12 @@ public class CategoryDao { * @param name Category's name * @return category from database, or null if not found */ - public @Nullable + @Nullable Category find(String name) { Cursor cursor = null; + ContentProviderClient db = clientProvider.get(); try { - cursor = client.query( + cursor = db.query( CategoryContentProvider.BASE_URI, Table.ALL_FIELDS, Table.COLUMN_NAME + "=?", @@ -60,6 +67,7 @@ public class CategoryDao { if (cursor != null) { cursor.close(); } + db.release(); } return null; } @@ -69,12 +77,13 @@ public class CategoryDao { * * @return a list containing recent categories */ - public @NonNull + @NonNull List recentCategories(int limit) { List items = new ArrayList<>(); Cursor cursor = null; + ContentProviderClient db = clientProvider.get(); try { - cursor = client.query( + cursor = db.query( CategoryContentProvider.BASE_URI, Table.ALL_FIELDS, null, @@ -91,6 +100,7 @@ public class CategoryDao { if (cursor != null) { cursor.close(); } + db.release(); } return items; } @@ -98,10 +108,10 @@ public class CategoryDao { 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) + CategoryContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))), + cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)), + new Date(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LAST_USED))), + cursor.getInt(cursor.getColumnIndex(Table.COLUMN_TIMES_USED)) ); } @@ -147,7 +157,7 @@ public class CategoryDao { onCreate(db); } - static void onUpdate(SQLiteDatabase db, int from, int to) { + public static void onUpdate(SQLiteDatabase db, int from, int to) { if (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 00baac847..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 @@ -197,6 +197,10 @@ public class Contribution extends Media { this.localUri = localUri; } + public void setDecimalCoords(String decimalCoords) { + this.decimalCoords = decimalCoords; + } + @NonNull private String licenseTemplateFor(String license) { switch (license) { @@ -215,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 a243330c3..37b3d5377 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 @@ -25,14 +25,14 @@ import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA; import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY; import static fr.free.nrw.commons.upload.UploadService.EXTRA_SOURCE; -class ContributionController { +public class ContributionController { private static final int SELECT_FROM_GALLERY = 1; private static final int SELECT_FROM_CAMERA = 2; private Fragment fragment; - ContributionController(Fragment fragment) { + public ContributionController(Fragment fragment) { this.fragment = fragment; } @@ -61,7 +61,7 @@ class ContributionController { } } - void startCameraCapture() { + public void startCameraCapture() { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); lastGeneratedCaptureUri = reGenerateImageCaptureUriInCache(); @@ -70,6 +70,9 @@ class ContributionController { requestWritePermission(fragment.getContext(), takePictureIntent, lastGeneratedCaptureUri); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, lastGeneratedCaptureUri); + if (!fragment.isAdded()) { + return; + } fragment.startActivityForResult(takePictureIntent, SELECT_FROM_CAMERA); } @@ -77,11 +80,19 @@ class ContributionController { //FIXME: Starts gallery (opens Google Photos) Intent pickImageIntent = new Intent(ACTION_GET_CONTENT); pickImageIntent.setType("image/*"); + // 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"); + fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY); } - void handleImagePicked(int requestCode, Intent data) { + public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload) { FragmentActivity activity = fragment.getActivity(); + Timber.d("handleImagePicked() called with onActivityResult()"); Intent shareIntent = new Intent(activity, ShareActivity.class); shareIntent.setAction(ACTION_SEND); switch (requestCode) { @@ -91,11 +102,23 @@ class ContributionController { shareIntent.setType(activity.getContentResolver().getType(imageData)); shareIntent.putExtra(EXTRA_STREAM, imageData); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY); + if (isDirectUpload) { + shareIntent.putExtra("isDirectUpload", true); + } 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); + } + + break; + default: break; } Timber.i("Image selected"); @@ -117,5 +140,4 @@ 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 index ffaf3fc8d..079cf6477 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java @@ -8,47 +8,85 @@ import android.net.Uri; import android.os.RemoteException; import android.support.annotation.Nullable; import android.text.TextUtils; +import android.util.Log; 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 { - private final ContentProviderClient client; + /* + 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) - public ContributionDao(ContentProviderClient client) { - this.client = client; + 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(client.insert(BASE_URI, toContentValues(contribution))); + contribution.setContentUri(db.insert(BASE_URI, toContentValues(contribution))); } else { - client.update(contribution.getContentUri(), toContentValues(contribution), null, null); + 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 { - client.delete(contribution.getContentUri(), null, null); + db.delete(contribution.getContentUri(), null, null); } } catch (RemoteException e) { throw new RuntimeException(e); + } finally { + db.release(); } } - public static ContentValues toContentValues(Contribution contribution) { + ContentValues toContentValues(Contribution contribution) { ContentValues cv = new ContentValues(); cv.put(Table.COLUMN_FILENAME, contribution.getFilename()); if (contribution.getLocalUri() != null) { @@ -74,27 +112,34 @@ public class ContributionDao { return cv; } - public static Contribution fromCursor(Cursor cursor) { + public Contribution fromCursor(Cursor cursor) { // Hardcoding column positions! //Check that cursor has a value to avoid CursorIndexOutOfBoundsException if (cursor.getCount() > 0) { + int index; + if (cursor.getColumnIndex(Table.COLUMN_LICENSE) == -1){ + index = 15; + } else { + index = cursor.getColumnIndex(Table.COLUMN_LICENSE); + } 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)); + uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))), + cursor.getString(cursor.getColumnIndex(Table.COLUMN_FILENAME)), + parseUri(cursor.getString(cursor.getColumnIndex(Table.COLUMN_LOCAL_URI))), + cursor.getString(cursor.getColumnIndex(Table.COLUMN_IMAGE_URL)), + parseTimestamp(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_TIMESTAMP))), + cursor.getInt(cursor.getColumnIndex(Table.COLUMN_STATE)), + cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LENGTH)), + parseTimestamp(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_UPLOADED))), + cursor.getLong(cursor.getColumnIndex(Table.COLUMN_TRANSFERRED)), + cursor.getString(cursor.getColumnIndex(Table.COLUMN_SOURCE)), + cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)), + cursor.getString(cursor.getColumnIndex(Table.COLUMN_CREATOR)), + cursor.getInt(cursor.getColumnIndex(Table.COLUMN_MULTIPLE)) == 1, + cursor.getInt(cursor.getColumnIndex(Table.COLUMN_WIDTH)), + cursor.getInt(cursor.getColumnIndex(Table.COLUMN_HEIGHT)), + cursor.getString(index) + ); } return null; 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 f1fa1d328..ad6cff606 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 @@ -26,6 +26,7 @@ import javax.inject.Inject; import javax.inject.Named; import butterknife.ButterKnife; +import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; @@ -43,7 +44,6 @@ 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.ContributionDao.Table.ALL_FIELDS; -import static fr.free.nrw.commons.contributions.ContributionsContentProvider.AUTHORITY; import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI; import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING; @@ -58,6 +58,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 +66,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 = ContributionDao.Table.COLUMN_STATE + " DESC, " - + ContributionDao.Table.COLUMN_UPLOADED + " DESC , (" - + ContributionDao.Table.COLUMN_TIMESTAMP + " * " - + ContributionDao.Table.COLUMN_STATE + ")"; private CompositeDisposable compositeDisposable = new CompositeDisposable(); @@ -121,14 +107,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); } @@ -155,7 +140,11 @@ public class ContributionsActivity requestAuthToken(); initDrawer(); setTitle(getString(R.string.title_activity_contributions)); - setUploadCount(); + + if(!BuildConfig.FLAVOR.equalsIgnoreCase("beta")){ + setUploadCount(); + } + } @Override @@ -186,7 +175,7 @@ public class ContributionsActivity public void retryUpload(int i) { allContributions.moveToPosition(i); - Contribution c = ContributionDao.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.toString()); @@ -197,10 +186,10 @@ public class ContributionsActivity public void deleteUpload(int i) { allContributions.moveToPosition(i); - Contribution c = ContributionDao.fromCursor(allContributions); + Contribution c = contributionDao.fromCursor(allContributions); if (c.getState() == STATE_FAILED) { Timber.d("Deleting failed contrib %s", c.toString()); - new ContributionDao(getContentResolver().acquireContentProviderClient(AUTHORITY)).delete(c); + contributionDao.delete(c); } else { Timber.d("Skipping deletion for non-failed contrib %s", c.toString()); } @@ -238,8 +227,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 @@ -248,7 +237,7 @@ public class ContributionsActivity if (contributionsList.getAdapter() == null) { contributionsList.setAdapter(new ContributionsListAdapter(getApplicationContext(), - cursor, 0)); + cursor, 0, contributionDao)); } else { ((CursorAdapter) contributionsList.getAdapter()).swapCursor(cursor); } @@ -269,7 +258,7 @@ public class ContributionsActivity // not yet ready to return data return null; } else { - return ContributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i)); + return contributionDao.fromCursor((Cursor) contributionsList.getAdapter().getItem(i)); } } @@ -295,6 +284,12 @@ public class ContributionsActivity )); } + public void betaSetUploadCount(int betaUploadCount){ + getSupportActionBar().setSubtitle(getResources() + .getQuantityString(R.plurals.contributions_subtitle, betaUploadCount, betaUploadCount)); + } + + @Override public void notifyDatasetChanged() { // Do nothing for now 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 402f91aaa..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.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); 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 781b3c4c4..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 = ContributionDao.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 590d4e6ad..ff400a8dd 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,16 @@ 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.BuildConfig; 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 +39,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,11 +48,16 @@ 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; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_contributions, container, false); @@ -81,6 +89,10 @@ public class ContributionsListFragment extends DaggerFragment { public void setAdapter(ListAdapter adapter) { this.contributionsList.setAdapter(adapter); + + if(BuildConfig.FLAVOR.equalsIgnoreCase("beta")){ + ((ContributionsActivity) getActivity()).betaSetUploadCount(adapter.getCount()); + } } public void changeProgressBarVisibility(boolean isVisible) { @@ -105,7 +117,7 @@ public class ContributionsListFragment extends DaggerFragment { if (resultCode == RESULT_OK) { Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); - controller.handleImagePicked(requestCode, data); + controller.handleImagePicked(requestCode, data, false); } else { Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); @@ -208,7 +220,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 4be42b1e0..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,8 +23,8 @@ 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; @@ -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(ContributionDao.toContentValues(contrib)); + imageValues.add(contributionDao.toContentValues(contrib)); if (imageValues.size() % COMMIT_THRESHOLD == 0) { try { 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 c8d33e3da..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,6 +4,7 @@ import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import fr.free.nrw.commons.category.CategoryDao; import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.modifications.ModifierSequenceDao; diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java new file mode 100644 index 000000000..07ae8cb81 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java @@ -0,0 +1,174 @@ +package fr.free.nrw.commons.delete; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.AsyncTask; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; + +import javax.inject.Inject; + +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import timber.log.Timber; + +import static android.support.v4.content.ContextCompat.startActivity; + +public class DeleteTask extends AsyncTask { + + private static final int SUCCESS = 0; + private static final int FAILED = -1; + private static final int ALREADY_DELETED = -2; + + @Inject MediaWikiApi mwApi; + @Inject SessionManager sessionManager; + + private Context context; + private Media media; + private String reason; + + public DeleteTask (Context context, Media media, String reason){ + this.context = context; + this.media = media; + this.reason = reason; + } + + @Override + protected void onPreExecute(){ + ApplicationlessInjection + .getInstance(context.getApplicationContext()) + .getCommonsApplicationComponent() + .inject(this); + } + + @Override + protected Integer doInBackground(Void ...voids) { + String editToken; + String authCookie; + String summary = "Nominating " + media.getFilename() +" for deletion."; + + authCookie = sessionManager.getAuthCookie(); + mwApi.setAuthCookie(authCookie); + + try{ + if (mwApi.pageExists("Commons:Deletion_requests/"+media.getFilename())){ + return ALREADY_DELETED; + } + } + catch (Exception e) { + Timber.d(e.getMessage()); + return FAILED; + } + + try { + editToken = mwApi.getEditToken(); + } + catch (Exception e){ + Timber.d(e.getMessage()); + return FAILED; + } + if (editToken.equals("+\\")) { + return FAILED; + } + + Calendar calendar = Calendar.getInstance(); + String fileDeleteString = "{{delete|reason=" + reason + + "|subpage=" +media.getFilename() + + "|day=" + calendar.get(Calendar.DAY_OF_MONTH) + + "|month=" + calendar.getDisplayName(Calendar.MONTH,Calendar.LONG, Locale.getDefault()) + + "|year=" + calendar.get(Calendar.YEAR) + + "}}"; + try{ + mwApi.prependEdit(editToken,fileDeleteString+"\n", + media.getFilename(),summary); + } + catch (Exception e) { + Timber.d(e.getMessage()); + return FAILED; + } + + String subpageString = "=== [[:" + media.getFilename() + "]] ===\n" + + reason + + " ~~~~"; + try{ + mwApi.edit(editToken,subpageString+"\n", + "Commons:Deletion_requests/"+media.getFilename(),summary); + } + catch (Exception e) { + Timber.d(e.getMessage()); + return FAILED; + } + + String logPageString = "\n{{Commons:Deletion requests/" + media.getFilename() + + "}}\n"; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); + String date = sdf.format(calendar.getTime()); + try{ + mwApi.appendEdit(editToken,logPageString+"\n", + "Commons:Deletion_requests/"+date,summary); + } + catch (Exception e) { + Timber.d(e.getMessage()); + return FAILED; + } + + String userPageString = "\n{{subst:idw|" + media.getFilename() + + "}} ~~~~"; + try{ + mwApi.appendEdit(editToken,userPageString+"\n", + "User_Talk:"+sessionManager.getCurrentAccount().name,summary); + } + catch (Exception e) { + Timber.d(e.getMessage()); + return FAILED; + } + return SUCCESS; + } + + @Override + protected void onPostExecute(Integer result) { + String message = ""; + String title = ""; + switch (result){ + case SUCCESS: + title = "Success"; + message = "Successfully nominated " + media.getDisplayTitle() + " deletion.\n" + + "Check the webpage for more details"; + break; + case FAILED: + title = "Failed"; + message = "Could not request deletion. Something went wrong."; + break; + case ALREADY_DELETED: + title = "Already Nominated"; + message = media.getDisplayTitle() + " has already been nominated for deletion.\n" + + "Check the webpage for more details"; + break; + } + AlertDialog alert; + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(title); + builder.setMessage(message); + builder.setCancelable(true); + builder.setPositiveButton( + R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) {} + }); + builder.setNeutralButton(R.string.view_browser, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, media.getFilePageTitle().getMobileUri()); + startActivity(context,browserIntent,null); + } + }); + alert = builder.create(); + alert.show(); + } +} \ No newline at end of file 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..91f6d4ccb 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,14 @@ 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.Contribution; +import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsSyncAdapter; +import fr.free.nrw.commons.delete.DeleteTask; 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 +27,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 +36,17 @@ public interface CommonsApplicationComponent extends AndroidInjector provideLruCache() { return new LruCache<>(1024); } -} +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java new file mode 100644 index 000000000..9f06725de --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java @@ -0,0 +1,43 @@ +package fr.free.nrw.commons.di; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; + +import javax.inject.Inject; + +import dagger.android.AndroidInjector; +import dagger.android.DispatchingAndroidInjector; +import dagger.android.support.HasSupportFragmentInjector; + +public abstract class CommonsDaggerAppCompatActivity extends AppCompatActivity implements HasSupportFragmentInjector { + + @Inject + DispatchingAndroidInjector supportFragmentInjector; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + inject(); + super.onCreate(savedInstanceState); + } + + @Override + public AndroidInjector supportFragmentInjector() { + return supportFragmentInjector; + } + + private void inject() { + ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext()); + + AndroidInjector activityInjector = injection.activityInjector(); + + if (activityInjector == null) { + throw new NullPointerException("ApplicationlessInjection.activityInjector() returned null"); + } + + activityInjector.inject(this); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java new file mode 100644 index 000000000..3c4cb9914 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java @@ -0,0 +1,31 @@ +package fr.free.nrw.commons.di; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import dagger.android.AndroidInjector; + +public abstract class CommonsDaggerBroadcastReceiver extends BroadcastReceiver { + + public CommonsDaggerBroadcastReceiver() { + super(); + } + + @Override + public void onReceive(Context context, Intent intent) { + inject(context); + } + + private void inject(Context context) { + ApplicationlessInjection injection = ApplicationlessInjection.getInstance(context.getApplicationContext()); + + AndroidInjector serviceInjector = injection.broadcastReceiverInjector(); + + if (serviceInjector == null) { + throw new NullPointerException("ApplicationlessInjection.broadcastReceiverInjector() returned null"); + } + serviceInjector.inject(this); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java new file mode 100644 index 000000000..38506c4ca --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java @@ -0,0 +1,32 @@ +package fr.free.nrw.commons.di; + +import android.content.ContentProvider; + +import dagger.android.AndroidInjector; + + +public abstract class CommonsDaggerContentProvider extends ContentProvider { + + public CommonsDaggerContentProvider() { + super(); + } + + @Override + public boolean onCreate() { + inject(); + return true; + } + + private void inject() { + ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getContext()); + + AndroidInjector serviceInjector = injection.contentProviderInjector(); + + if (serviceInjector == null) { + throw new NullPointerException("ApplicationlessInjection.contentProviderInjector() returned null"); + } + + serviceInjector.inject(this); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java new file mode 100644 index 000000000..995c517a1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java @@ -0,0 +1,32 @@ +package fr.free.nrw.commons.di; + +import android.app.IntentService; +import android.app.Service; + +import dagger.android.AndroidInjector; + +public abstract class CommonsDaggerIntentService extends IntentService { + + public CommonsDaggerIntentService(String name) { + super(name); + } + + @Override + public void onCreate() { + inject(); + super.onCreate(); + } + + private void inject() { + ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext()); + + AndroidInjector serviceInjector = injection.serviceInjector(); + + if (serviceInjector == null) { + throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null"); + } + + serviceInjector.inject(this); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java new file mode 100644 index 000000000..dc6c10b9f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java @@ -0,0 +1,31 @@ +package fr.free.nrw.commons.di; + +import android.app.Service; + +import dagger.android.AndroidInjector; + +public abstract class CommonsDaggerService extends Service { + + public CommonsDaggerService() { + super(); + } + + @Override + public void onCreate() { + inject(); + super.onCreate(); + } + + private void inject() { + ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext()); + + AndroidInjector serviceInjector = injection.serviceInjector(); + + if (serviceInjector == null) { + throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null"); + } + + serviceInjector.inject(this); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java new file mode 100644 index 000000000..8c33e7a98 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java @@ -0,0 +1,65 @@ +package fr.free.nrw.commons.di; + +import android.app.Activity; +import android.content.Context; +import android.support.v4.app.Fragment; + +import javax.inject.Inject; + +import dagger.android.AndroidInjector; +import dagger.android.DispatchingAndroidInjector; +import dagger.android.support.HasSupportFragmentInjector; + +public abstract class CommonsDaggerSupportFragment extends Fragment implements HasSupportFragmentInjector { + + @Inject + DispatchingAndroidInjector childFragmentInjector; + + @Override + public void onAttach(Context context) { + inject(); + super.onAttach(context); + } + + @Override + public AndroidInjector supportFragmentInjector() { + return childFragmentInjector; + } + + + public void inject() { + HasSupportFragmentInjector hasSupportFragmentInjector = findHasFragmentInjector(); + + AndroidInjector fragmentInjector = hasSupportFragmentInjector.supportFragmentInjector(); + + if (fragmentInjector == null) { + throw new NullPointerException(String.format("%s.supportFragmentInjector() returned null", hasSupportFragmentInjector.getClass().getCanonicalName())); + } + + fragmentInjector.inject(this); + } + + private HasSupportFragmentInjector findHasFragmentInjector() { + Fragment parentFragment = this; + + while ((parentFragment = parentFragment.getParentFragment()) != null) { + if (parentFragment instanceof HasSupportFragmentInjector) { + return (HasSupportFragmentInjector) parentFragment; + } + } + + Activity activity = getActivity(); + + if (activity instanceof HasSupportFragmentInjector) { + return (HasSupportFragmentInjector) activity; + } + + ApplicationlessInjection injection = ApplicationlessInjection.getInstance(activity.getApplicationContext()); + if (injection != null) { + return injection; + } + + throw new IllegalArgumentException(String.format("No injector was found for %s", getClass().getCanonicalName())); + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index 5348bc581..c5cdcb5a7 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -8,6 +8,7 @@ import fr.free.nrw.commons.featured.FeaturedImagesListFragment; import fr.free.nrw.commons.media.MediaDetailFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.nearby.NearbyListFragment; +import fr.free.nrw.commons.nearby.NearbyMapFragment; import fr.free.nrw.commons.nearby.NoPermissionsFragment; import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.upload.MultipleUploadListFragment; @@ -32,6 +33,9 @@ public abstract class FragmentBuilderModule { @ContributesAndroidInjector abstract NearbyListFragment bindNearbyListFragment(); + @ContributesAndroidInjector + abstract NearbyMapFragment bindNearbyMapFragment(); + @ContributesAndroidInjector abstract NoPermissionsFragment bindNoPermissionsFragment(); 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 d587b88e3..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,6 +1,7 @@ package fr.free.nrw.commons.location; import android.location.Location; +import android.net.Uri; import android.support.annotation.NonNull; /** @@ -11,15 +12,15 @@ 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) @@ -38,23 +39,22 @@ public class LatLng { /** * gets the latitude and longitude of a given non-null location * @param location the non-null location of the user - * @return + * @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() { - 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; + 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; } /** @@ -154,4 +154,9 @@ public class LatLng { 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 d949189ed..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 @@ -10,19 +10,18 @@ import android.location.LocationManager; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; +import android.util.Log; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -import javax.inject.Inject; -import javax.inject.Singleton; - import timber.log.Timber; public class LocationServiceManager implements LocationListener { public static final int LOCATION_REQUEST = 1; - private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 1000; + // Maybe these values can be improved for efficiency + private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 2 * 60 * 100; private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 10; private Context context; @@ -33,6 +32,7 @@ public class LocationServiceManager implements LocationListener { /** * Constructs a new instance of LocationServiceManager. + * * @param context the context */ public LocationServiceManager(Context context) { @@ -42,6 +42,7 @@ public class LocationServiceManager implements LocationListener { /** * Returns the current status of the GPS provider. + * * @return true if the GPS provider is enabled */ public boolean isProviderEnabled() { @@ -50,6 +51,7 @@ public class LocationServiceManager implements LocationListener { /** * Returns whether the location permission is granted. + * * @return true if the location permission is granted */ public boolean isLocationPermissionGranted() { @@ -59,6 +61,7 @@ public class LocationServiceManager implements LocationListener { /** * Requests the location permission to be granted. + * * @param activity the activity */ public void requestPermissions(Activity activity) { @@ -71,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() { @@ -85,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) @@ -95,6 +97,7 @@ public class LocationServiceManager implements LocationListener { /** * Requests location updates from the specified provider. + * * @param locationProvider the location provider * @return true if successful */ @@ -116,14 +119,17 @@ 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 location the location to be tested * @param currentBestLocation the current best location - * @return true if the given location is better + * @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly + * LOCATION_SLIGHTLY_CHANGED if location changed slightly */ - protected boolean isBetterLocation(Location location, Location currentBestLocation) { + protected LocationChangeType isBetterLocation(Location location, Location currentBestLocation) { + if (currentBestLocation == null) { // A new location is always better than no location - return true; + return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; } // Check whether the new location fix is newer or older @@ -132,15 +138,6 @@ public class LocationServiceManager implements LocationListener { boolean isSignificantlyOlder = timeDelta < -MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS; boolean isNewer = timeDelta > 0; - // If it's been more than two minutes since the current location, use the new location - // because the user has likely moved - if (isSignificantlyNewer) { - return true; - // If the new location is more than two minutes older, it must be worse - } else if (isSignificantlyOlder) { - return false; - } - // Check whether the new location fix is more or less accurate int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); boolean isLessAccurate = accuracyDelta > 0; @@ -151,15 +148,28 @@ public class LocationServiceManager implements LocationListener { boolean isFromSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider()); - // Determine location quality using a combination of timeliness and accuracy - if (isMoreAccurate) { - return true; - } else if (isNewer && !isLessAccurate) { - return true; - } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) { - return true; + float[] results = new float[5]; + Location.distanceBetween( + currentBestLocation.getLatitude(), + currentBestLocation.getLongitude(), + location.getLatitude(), + location.getLongitude(), + results); + + // If it's been more than two minutes since the current location, use the new location + // because the user has likely moved + if (isSignificantlyNewer + || isMoreAccurate + || (isNewer && !isLessAccurate) + || (isNewer && !isSignificantlyLessAccurate && isFromSameProvider)) { + if (results[0] < 1000) { // Means change is smaller than 1000 meter + return LocationChangeType.LOCATION_SLIGHTLY_CHANGED; + } else { + return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; + } + } else{ + return LocationChangeType.LOCATION_NOT_CHANGED; } - return false; } /** @@ -172,7 +182,8 @@ public class LocationServiceManager implements LocationListener { return provider1.equals(provider2); } - /** Unregisters location manager. + /** + * Unregisters location manager. */ public void unregisterLocationManager() { isLocationManagerRegistered = false; @@ -185,6 +196,7 @@ 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) { @@ -195,6 +207,7 @@ public class LocationServiceManager implements LocationListener { /** * Removes a listener from the list of location listeners. + * * @param listener the listener to be removed */ public void removeLocationListener(LocationUpdateListener listener) { @@ -203,12 +216,19 @@ public class LocationServiceManager implements LocationListener { @Override public void onLocationChanged(Location location) { - if (isBetterLocation(location, lastLocation)) { - lastLocation = location; - for (LocationUpdateListener listener : locationListeners) { - listener.onLocationChanged(LatLng.from(lastLocation)); + if (isBetterLocation(location, lastLocation) + .equals(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) { + lastLocation = location; + for (LocationUpdateListener listener : locationListeners) { + listener.onLocationChangedSignificantly(LatLng.from(lastLocation)); + } + } else if (isBetterLocation(location, lastLocation) + .equals(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) { + lastLocation = location; + for (LocationUpdateListener listener : locationListeners) { + listener.onLocationChangedSlightly(LatLng.from(lastLocation)); + } } - } } @Override @@ -225,4 +245,10 @@ public class LocationServiceManager implements LocationListener { public void onProviderDisabled(String provider) { Timber.d("Provider %s disabled", provider); } + + public enum LocationChangeType{ + LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers + LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving + LOCATION_NOT_CHANGED + } } diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java index 69d3048a1..f3e920e18 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.location; public interface LocationUpdateListener { - void onLocationChanged(LatLng latLng); + void onLocationChangedSignificantly(LatLng latLng); + void onLocationChangedSlightly(LatLng latLng); } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index aeb28669b..70dcc780e 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 @@ -1,19 +1,27 @@ package fr.free.nrw.commons.media; +import android.app.AlertDialog; +import android.content.DialogInterface; import android.content.Intent; import android.database.DataSetObserver; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; +import android.text.Editable; +import android.text.TextWatcher; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; +import android.widget.Toast; import java.io.IOException; import java.text.SimpleDateFormat; @@ -24,7 +32,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 +39,15 @@ 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.delete.DeleteTask; +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 { +import static android.widget.Toast.LENGTH_SHORT; + +public class MediaDetailFragment extends CommonsDaggerSupportFragment { private boolean editable; private boolean isFeaturedMedia; @@ -74,6 +84,7 @@ public class MediaDetailFragment extends DaggerFragment { private TextView uploadedDate; private LinearLayout categoryContainer; private LinearLayout authorLayout; + private Button delete; private ScrollView scrollView; private ArrayList categoryNames; private boolean categoriesLoaded = false; @@ -81,7 +92,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 @@ -101,7 +112,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"); @@ -131,6 +142,7 @@ public class MediaDetailFragment extends DaggerFragment { license = (TextView) view.findViewById(R.id.mediaDetailLicense); coordinates = (TextView) view.findViewById(R.id.mediaDetailCoordinates); uploadedDate = (TextView) view.findViewById(R.id.mediaDetailuploadeddate); + delete = (Button) view.findViewById(R.id.nominateDeletion); categoryContainer = (LinearLayout) view.findViewById(R.id.mediaDetailCategoryContainer); authorLayout = (LinearLayout) view.findViewById(R.id.authorLinearLayout); @@ -173,7 +185,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) { @@ -255,13 +268,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); @@ -286,13 +299,66 @@ public class MediaDetailFragment extends DaggerFragment { categoryNames.add(getString(R.string.detail_panel_cats_none)); } rebuildCatList(); + + delete.setVisibility(View.VISIBLE); } private void setOnClickListeners(final Media media) { - license.setOnClickListener(v -> openWebBrowser(licenseLink(media))); + if (licenseLink(media) != null) { + license.setOnClickListener(v -> openWebBrowser(licenseLink(media))); + } else { + Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT); + toast.show(); + } if (media.getCoordinates() != null) { coordinates.setOnClickListener(v -> openMap(media.getCoordinates())); } + if (delete.getVisibility()==View.VISIBLE){ + delete.setOnClickListener(v -> { + AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); + alert.setMessage("Why should this file be deleted?"); + final EditText input = new EditText(getActivity()); + alert.setView(input); + input.requestFocus(); + alert.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + String reason = input.getText().toString(); + DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason); + deleteTask.execute(); + } + }); + alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + } + }); + AlertDialog d = alert.create(); + input.addTextChangedListener(new TextWatcher() { + private void handleText() { + final Button okButton = d.getButton(AlertDialog.BUTTON_POSITIVE); + if (input.getText().length() == 0) { + okButton.setEnabled(false); + } else { + okButton.setEnabled(true); + } + } + + @Override + public void afterTextChanged(Editable arg0) { + handleText(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }); + d.show(); + d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + }); + } } private void rebuildCatList() { @@ -306,7 +372,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) { @@ -315,7 +381,13 @@ public class MediaDetailFragment extends DaggerFragment { Intent viewIntent = new Intent(); viewIntent.setAction(Intent.ACTION_VIEW); viewIntent.setData(new PageTitle(selectedCategoryTitle).getCanonicalUri()); - startActivity(viewIntent); + //check if web browser available + if(viewIntent.resolveActivity(getActivity().getPackageManager()) != null){ + startActivity(viewIntent); + } else { + Toast toast = Toast.makeText(getContext(), getString(R.string.no_web_browser), LENGTH_SHORT); + toast.show(); + } }); } return item; @@ -325,7 +397,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; @@ -379,7 +451,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; @@ -394,7 +467,14 @@ public class MediaDetailFragment extends DaggerFragment { private void openWebBrowser(String url) { Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(browser); + //check if web browser available + if (browser.resolveActivity(getActivity().getPackageManager()) != null) { + startActivity(browser); + } else { + Toast toast = Toast.makeText(getContext(), getString(R.string.no_web_browser), LENGTH_SHORT); + toast.show(); + } + } private void openMap(LatLng coordinates) { 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 c067a9ce3..c0564c603 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 @@ -24,28 +24,34 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; 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; import static android.content.Context.DOWNLOAD_SERVICE; import static android.content.Intent.ACTION_VIEW; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.widget.Toast.LENGTH_SHORT; -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; @@ -118,7 +124,14 @@ public class MediaDetailPagerFragment extends DaggerFragment implements ViewPage Intent viewIntent = new Intent(); viewIntent.setAction(ACTION_VIEW); viewIntent.setData(m.getFilePageTitle().getMobileUri()); - startActivity(viewIntent); + //check if web browser available + if(viewIntent.resolveActivity(getActivity().getPackageManager()) != null){ + startActivity(viewIntent); + } else { + Toast toast = Toast.makeText(getContext(), getString(R.string.no_web_browser), LENGTH_SHORT); + toast.show(); + } + return true; case R.id.menu_download_current_image: // Download @@ -168,13 +181,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 e1877e79c..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,26 +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; import static fr.free.nrw.commons.modifications.ModifierSequenceDao.Table.TABLE_NAME; -public class ModificationsContentProvider extends ContentProvider { +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"; + 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) { @@ -40,12 +39,6 @@ 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(); @@ -77,7 +70,7 @@ 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(TABLE_NAME, null, contentValues); @@ -139,7 +132,7 @@ 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(TABLE_NAME, 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 d000a2ed5..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,16 +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); @@ -34,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 { @@ -49,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; @@ -80,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 = ModifierSequenceDao.fromCursor(allModifications); - ModifierSequenceDao dao = new ModifierSequenceDao(contributionsClient); + 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 = ContributionDao.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; } @@ -112,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 { - dao.delete(sequence); + 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 93cb3bc3d..c9858a8e3 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 @@ -45,7 +45,7 @@ public class ModifierSequence { for (PageModifier modifier: modifiers) { editSummary.append(modifier.getEditSumary()).append(" "); } - editSummary.append("Via Commons Mobile App"); + editSummary.append("Using [[COM:MOA|Commons Mobile App]]"); return editSummary.toString(); } 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 index c98081c72..957656a24 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java @@ -11,48 +11,59 @@ 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 ContentProviderClient client; + private final Provider clientProvider; - public ModifierSequenceDao(ContentProviderClient client) { - this.client = client; - } - - 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.setContentUri( ModificationsContentProvider.uriForId(cursor.getInt(0))); - - return ms; + @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(client.insert(ModificationsContentProvider.BASE_URI, toContentValues(sequence))); + sequence.setContentUri(db.insert(ModificationsContentProvider.BASE_URI, toContentValues(sequence))); } else { - client.update(sequence.getContentUri(), toContentValues(sequence), null, null); + 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 { - client.delete(sequence.getContentUri(), null, null); + 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(cursor.getColumnIndex(Table.COLUMN_MEDIA_URI))), + new JSONObject(cursor.getString(cursor.getColumnIndex(Table.COLUMN_DATA)))); + } catch (JSONException e) { + throw new RuntimeException(e); + } + ms.setContentUri( ModificationsContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID)))); + + return ms; + } + private JSONObject toJSON(ModifierSequence sequence) { JSONObject data = new JSONObject(); try { 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..32ca96174 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(); @@ -166,6 +205,14 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { .getNodes("/api/query/pages/page/imageinfo").size() > 0; } + @Override + public boolean pageExists(String pageName) throws IOException { + return Double.parseDouble( api.action("query") + .param("titles", pageName) + .get() + .getString("/api/query/pages/page/@_idx")) != -1; + } + @Override @Nullable public String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException { @@ -178,6 +225,31 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { .getString("/api/edit/@result"); } + + @Override + @Nullable + public String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException { + return api.action("edit") + .param("title", filename) + .param("token", editToken) + .param("appendtext", processedPageContent) + .param("summary", summary) + .post() + .getString("/api/edit/@result"); + } + + @Override + @Nullable + public String prependEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException { + return api.action("edit") + .param("title", filename) + .param("token", editToken) + .param("prependtext", processedPageContent) + .param("summary", summary) + .post() + .getString("/api/edit/@result"); + } + @Override public String findThumbnailByFilename(String filename) throws IOException { return api.action("query") @@ -353,6 +425,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..fd213455d 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); @@ -24,6 +28,8 @@ public interface MediaWikiApi { boolean fileExistsWithName(String fileName) throws IOException; + boolean pageExists(String pageName) throws IOException; + String findThumbnailByFilename(String filename) throws IOException; boolean logEvents(LogBuilder[] logBuilders); @@ -34,6 +40,12 @@ public interface MediaWikiApi { @Nullable String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException; + @Nullable + String prependEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException; + + @Nullable + String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException; + @NonNull MediaResult fetchMediaByFilename(String filename) throws IOException; @@ -43,6 +55,9 @@ public interface MediaWikiApi { @NonNull Observable allCategories(String filter, int searchCatsLimit); + @NonNull + List getNotifications() throws IOException; + @NonNull Observable searchTitles(String title, int searchCatsLimit); 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 new file mode 100644 index 000000000..b58afa82a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java @@ -0,0 +1,75 @@ +package fr.free.nrw.commons.nearby; + +import android.content.SharedPreferences; +import android.os.Build; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AlertDialog; + +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.contributions.ContributionController; + +import static android.Manifest.permission.READ_EXTERNAL_STORAGE; +import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +class DirectUpload { + + private ContributionController controller; + private Fragment fragment; + + DirectUpload(Fragment fragment, ContributionController controller) { + this.fragment = fragment; + this.controller = controller; + } + + void initiateCameraUpload() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(fragment.getActivity(), WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) { + if (fragment.getActivity().shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { + new AlertDialog.Builder(fragment.getActivity()) + .setMessage(fragment.getActivity().getString(R.string.write_storage_permission_rationale)) + .setPositiveButton("OK", (dialog, which) -> { + fragment.getActivity().requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 3); + dialog.dismiss(); + }) + .setNegativeButton("Cancel", null) + .create() + .show(); + } else { + fragment.getActivity().requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 3); + } + } else { + controller.startCameraCapture(); + } + } else { + controller.startCameraCapture(); + } + } + + void initiateGalleryUpload() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(fragment.getActivity(), READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) { + if (fragment.getActivity().shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE)) { + new AlertDialog.Builder(fragment.getActivity()) + .setMessage(fragment.getActivity().getString(R.string.read_storage_permission_rationale)) + .setPositiveButton("OK", (dialog, which) -> { + fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, 1); + dialog.dismiss(); + }) + .setNegativeButton("Cancel", null) + .create() + .show(); + } else { + fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, + 1); + } + } else { + controller.startGalleryPick(); + } + } + else { + controller.startGalleryPick(); + } + } +} 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 57d21dd95..ed954ed42 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java @@ -1,21 +1,20 @@ package fr.free.nrw.commons.nearby; -import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.preference.PreferenceManager; import android.support.annotation.NonNull; -import android.support.v4.app.Fragment; +import android.support.design.widget.BottomSheetBehavior; + import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AlertDialog; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.Toast; @@ -28,30 +27,37 @@ import javax.inject.Inject; import butterknife.BindView; import butterknife.ButterKnife; + import fr.free.nrw.commons.R; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.UriSerializer; + import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; -import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST; +import timber.log.Timber; public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener { private static final int LOCATION_REQUEST = 1; - private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed"; @BindView(R.id.progressBar) ProgressBar progressBar; + @BindView(R.id.bottom_sheet) + LinearLayout bottomSheet; + @BindView(R.id.bottom_sheet_details) + LinearLayout bottomSheetDetails; + @BindView(R.id.transparentView) + View transparentView; + @Inject LocationServiceManager locationManager; @Inject @@ -59,28 +65,57 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp private LatLng curLatLang; private Bundle bundle; - private SharedPreferences sharedPreferences; - private NearbyActivityMode viewMode; private Disposable placesDisposable; private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed + private BottomSheetBehavior bottomSheetBehavior; // Behavior for list bottom sheet + private BottomSheetBehavior bottomSheetBehaviorForDetails; // Behavior for details bottom sheet + private NearbyMapFragment nearbyMapFragment; + private NearbyListFragment nearbyListFragment; + private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName(); + private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); setContentView(R.layout.activity_nearby); ButterKnife.bind(this); + resumeFragment(); bundle = new Bundle(); + + initBottomSheetBehaviour(); initDrawer(); - initViewState(); } - private void initViewState() { - if (sharedPreferences.getBoolean(MAP_LAST_USED_PREFERENCE, false)) { - viewMode = NearbyActivityMode.MAP; - } else { - viewMode = NearbyActivityMode.LIST; - } + private void resumeFragment() { + // Find the retained fragment on activity restarts + nearbyMapFragment = getMapFragment(); + nearbyListFragment = getListFragment(); + } + + private void initBottomSheetBehaviour() { + + transparentView.setAlpha(0); + + bottomSheet.getLayoutParams().height = getWindowManager() + .getDefaultDisplay().getHeight() / 16 * 9; + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet); + // TODO initProperBottomSheetBehavior(); + bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { + + @Override + public void onStateChanged(View bottomSheet, int newState) { + prepareViewsForSheetPosition(newState); + } + + @Override + public void onSlide(View bottomSheet, float slideOffset) { + + } + }); + + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetBehaviorForDetails = BottomSheetBehavior.from(bottomSheetDetails); + bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); } @Override @@ -88,11 +123,6 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_nearby, menu); - if (viewMode.isMap()) { - MenuItem item = menu.findItem(R.id.action_toggle_view); - item.setIcon(viewMode.getIcon()); - } - return super.onCreateOptionsMenu(menu); } @@ -100,14 +130,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection switch (item.getItemId()) { - case R.id.action_refresh: - lockNearbyView(false); - refreshView(true); - return true; - case R.id.action_toggle_view: - viewMode = viewMode.toggle(); - item.setIcon(viewMode.getIcon()); - toggleView(); + case R.id.action_display_list: + bottomSheetBehaviorForDetails.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); return true; default: return super.onOptionsItemSelected(item); @@ -125,7 +150,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp switch (requestCode) { case LOCATION_REQUEST: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - refreshView(false); + refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); } else { //If permission not granted, go to page that says Nearby Places cannot be displayed hideProgressBar(); @@ -180,7 +205,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp private void checkLocationPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (locationManager.isLocationPermissionGranted()) { - refreshView(false); + refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); } else { // Should we show an explanation? if (locationManager.isPermissionExplanationRequired(this)) { @@ -206,7 +231,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } } } else { - refreshView(false); + refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); } } @@ -215,23 +240,15 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp super.onActivityResult(requestCode, resultCode, data); if (requestCode == 1) { Timber.d("User is back from Settings page"); - refreshView(false); + refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); } } - private void toggleView() { - if (viewMode.isMap()) { - setMapFragment(); - } else { - setListFragment(); - } - sharedPreferences.edit().putBoolean(MAP_LAST_USED_PREFERENCE, viewMode.isMap()).apply(); - } - @Override protected void onStart() { super.onStart(); locationManager.addLocationListener(this); + locationManager.registerLocationManager(); } @Override @@ -256,21 +273,34 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp checkGps(); } + @Override + public void onPause() { + super.onPause(); + // this means that this activity will not be recreated now, user is leaving it + // or the activity is otherwise finishing + if(isFinishing()) { + // we will not need this fragment anymore, this may also be a good place to signal + // to the retained fragment object to perform its own cleanup. + removeMapFragment(); + removeListFragment(); + } + } + + + /** * This method should be the single point to load/refresh nearby places * - * @param isHardRefresh + * @param locationChangeType defines if location shanged significantly or slightly */ - private void refreshView(boolean isHardRefresh) { + private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) { if (lockNearbyView) { return; } locationManager.registerLocationManager(); LatLng lastLocation = locationManager.getLastLocation(); + if (curLatLang != null && curLatLang.equals(lastLocation)) { //refresh view only if location has changed - if (isHardRefresh) { - ViewUtil.showLongToast(this, R.string.nearby_location_has_not_changed); - } return; } curLatLang = lastLocation; @@ -280,40 +310,56 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp return; } - progressBar.setVisibility(View.VISIBLE); - placesDisposable = Observable.fromCallable(() -> nearbyController - .loadAttractionsFromLocation(curLatLang, this)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::populatePlaces); + if (locationChangeType + .equals(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) { + progressBar.setVisibility(View.VISIBLE); + placesDisposable = Observable.fromCallable(() -> nearbyController + .loadAttractionsFromLocation(curLatLang)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::populatePlaces); + } else if (locationChangeType + .equals(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Uri.class, new UriSerializer()) + .create(); + String gsonCurLatLng = gson.toJson(curLatLang); + bundle.putString("CurLatLng", gsonCurLatLng); + updateMapFragment(true); + } } - private void populatePlaces(List placeList) { + private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { + List placeList = nearbyPlacesInfo.placeList; + LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates; Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriSerializer()) .create(); String gsonPlaceList = gson.toJson(placeList); String gsonCurLatLng = gson.toJson(curLatLang); + String gsonBoundaryCoordinates = gson.toJson(boundaryCoordinates); if (placeList.size() == 0) { - int duration = Toast.LENGTH_SHORT; - Toast toast = Toast.makeText(this, R.string.no_nearby, duration); - toast.show(); + ViewUtil.showSnackbar(findViewById(R.id.container), R.string.no_nearby); } bundle.clear(); bundle.putString("PlaceList", gsonPlaceList); bundle.putString("CurLatLng", gsonCurLatLng); + bundle.putString("BoundaryCoord", gsonBoundaryCoordinates); - lockNearbyView(true); - // Begin the transaction - if (viewMode.isMap()) { + // First time to init fragments + if (nearbyMapFragment == null) { + lockNearbyView(true); setMapFragment(); - } else { setListFragment(); + hideProgressBar(); + lockNearbyView(false); + } else { + // There are fragments, just update the map and list + updateMapFragment(false); + updateListFragment(); } - - hideProgressBar(); } private void lockNearbyView(boolean lock) { @@ -334,14 +380,92 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } } + private NearbyMapFragment getMapFragment() { + return (NearbyMapFragment) getSupportFragmentManager().findFragmentByTag(TAG_RETAINED_MAP_FRAGMENT); + } + + private void removeMapFragment() { + if (nearbyMapFragment != null) { + android.support.v4.app.FragmentManager fm = getSupportFragmentManager(); + fm.beginTransaction().remove(nearbyMapFragment).commit(); + } + } + + private NearbyListFragment getListFragment() { + return (NearbyListFragment) getSupportFragmentManager().findFragmentByTag(TAG_RETAINED_LIST_FRAGMENT); + } + + private void removeListFragment() { + if (nearbyListFragment != null) { + android.support.v4.app.FragmentManager fm = getSupportFragmentManager(); + fm.beginTransaction().remove(nearbyListFragment).commit(); + } + } + + private void updateMapFragment(boolean isSlightUpdate) { + /* + * Significant update means updating nearby place markers. Slightly update means only + * updating current location marker and camera target. + * We update our map Significantly on each 1000 meter change, but we can't never know + * the frequency of nearby places. Thus we check if we are close to the boundaries of + * our nearby markers, we update our map Significantly. + * */ + + NearbyMapFragment nearbyMapFragment = getMapFragment(); + + if (nearbyMapFragment != null && curLatLang != null) { + hideProgressBar(); // In case it is visible (this happens, not an impossible case) + /* + * If we are close to nearby places boundaries, we need a significant update to + * get new nearby places. Check order is south, north, west, east + * */ + if (nearbyMapFragment.boundaryCoordinates != null + && (curLatLang.getLatitude() <= nearbyMapFragment.boundaryCoordinates[0].getLatitude() + || curLatLang.getLatitude() >= nearbyMapFragment.boundaryCoordinates[1].getLatitude() + || curLatLang.getLongitude() <= nearbyMapFragment.boundaryCoordinates[2].getLongitude() + || curLatLang.getLongitude() >= nearbyMapFragment.boundaryCoordinates[3].getLongitude())) { + // populate places + placesDisposable = Observable.fromCallable(() -> nearbyController + .loadAttractionsFromLocation(curLatLang)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::populatePlaces); + nearbyMapFragment.setArguments(bundle); + nearbyMapFragment.updateMapSignificantly(); + updateListFragment(); + return; + } + + if (isSlightUpdate) { + nearbyMapFragment.setArguments(bundle); + nearbyMapFragment.updateMapSlightly(); + } else { + nearbyMapFragment.setArguments(bundle); + nearbyMapFragment.updateMapSignificantly(); + updateListFragment(); + } + } else { + lockNearbyView(true); + setMapFragment(); + setListFragment(); + hideProgressBar(); + lockNearbyView(false); + } + } + + private void updateListFragment() { + nearbyListFragment.setArguments(bundle); + nearbyListFragment.updateNearbyListSignificantly(); + } + /** * Calls fragment for map view. */ private void setMapFragment() { FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); - Fragment fragment = new NearbyMapFragment(); - fragment.setArguments(bundle); - fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName()); + nearbyMapFragment = new NearbyMapFragment(); + nearbyMapFragment.setArguments(bundle); + fragmentTransaction.replace(R.id.container, nearbyMapFragment, TAG_RETAINED_MAP_FRAGMENT); fragmentTransaction.commitAllowingStateLoss(); } @@ -350,14 +474,25 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp */ private void setListFragment() { FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); - Fragment fragment = new NearbyListFragment(); - fragment.setArguments(bundle); - fragmentTransaction.replace(R.id.container, fragment, fragment.getClass().getSimpleName()); + nearbyListFragment = new NearbyListFragment(); + nearbyListFragment.setArguments(bundle); + fragmentTransaction.replace(R.id.container_sheet, nearbyListFragment, TAG_RETAINED_LIST_FRAGMENT); + initBottomSheetBehaviour(); + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); fragmentTransaction.commitAllowingStateLoss(); } @Override - public void onLocationChanged(LatLng latLng) { - refreshView(false); + public void onLocationChangedSignificantly(LatLng latLng) { + refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + } + + @Override + public void onLocationChangedSlightly(LatLng latLng) { + refreshView(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED); + } + + public void prepareViewsForSheetPosition(int bottomSheetState) { + // TODO } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivityMode.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivityMode.java deleted file mode 100644 index e46f53f66..000000000 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivityMode.java +++ /dev/null @@ -1,30 +0,0 @@ -package fr.free.nrw.commons.nearby; - -import android.support.annotation.DrawableRes; - -import fr.free.nrw.commons.R; - -enum NearbyActivityMode { - MAP(R.drawable.ic_list_white_24dp), - LIST(R.drawable.ic_map_white_24dp); - - @DrawableRes - private final int icon; - - NearbyActivityMode(int icon) { - this.icon = icon; - } - - @DrawableRes - public int getIcon() { - return icon; - } - - public NearbyActivityMode toggle() { - return isMap() ? LIST : MAP; - } - - public boolean isMap() { - return MAP.equals(this); - } -} \ No newline at end of file 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 d5eb05851..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,6 +1,7 @@ package fr.free.nrw.commons.nearby; -import android.support.annotation.NonNull; + +import android.support.v4.app.Fragment; import com.pedrogomez.renderers.ListAdapteeCollection; import com.pedrogomez.renderers.RVRendererAdapter; @@ -9,18 +10,32 @@ import com.pedrogomez.renderers.RendererBuilder; import java.util.Collections; import java.util.List; -class NearbyAdapterFactory { - private PlaceRenderer.PlaceClickedListener listener; +import fr.free.nrw.commons.contributions.ContributionController; - NearbyAdapterFactory(@NonNull PlaceRenderer.PlaceClickedListener listener) { - this.listener = listener; +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(listener)); + .bind(Place.class, new PlaceRenderer(fragment, controller)); ListAdapteeCollection collection = new ListAdapteeCollection<>( - placeList != null ? placeList : Collections.emptyList()); + placeList != null ? placeList : Collections.emptyList()); return new RVRendererAdapter<>(builder, collection); } -} + + public void updateAdapterData(List newPlaceList, RVRendererAdapter rendererAdapter) { + rendererAdapter.notifyDataSetChanged(); + rendererAdapter.diffUpdate(newPlaceList); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyBaseMarker.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyBaseMarker.java index e8f1b0da0..22cd55654 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyBaseMarker.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyBaseMarker.java @@ -37,7 +37,7 @@ public class NearbyBaseMarker extends BaseMarkerOptions loadAttractionsFromLocation(LatLng curLatLng, Context context) { + public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) { + Timber.d("Loading attractions near %s", curLatLng); + NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo(); + if (curLatLng == null) { - return Collections.emptyList(); + return null; } - List places = prefs.getBoolean("useWikidata", true) - ? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()) - : nearbyPlaces.getFromWikiNeedsPictures(); + 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<>(); for (Place place: places) { distances.put(place, computeDistanceBetween(place.location, curLatLng)); + // Find boundaries with basic find max approach + if (place.location.getLatitude() < boundaryCoordinates[0].getLatitude()) { + boundaryCoordinates[0] = place.location; + } + if (place.location.getLatitude() > boundaryCoordinates[1].getLatitude()) { + boundaryCoordinates[1] = place.location; + } + if (place.location.getLongitude() < boundaryCoordinates[2].getLongitude()) { + boundaryCoordinates[2] = place.location; + } + if (place.location.getLongitude() > boundaryCoordinates[3].getLongitude()) { + boundaryCoordinates[3] = place.location; + } } Collections.sort(places, (lhs, rhs) -> { @@ -65,11 +86,14 @@ public class NearbyController { } ); } - return places; + nearbyPlacesInfo.placeList = places; + nearbyPlacesInfo.boundaryCoordinates = boundaryCoordinates; + return nearbyPlacesInfo; } /** * 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 @@ -78,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); } @@ -86,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 @@ -103,27 +128,34 @@ 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; } + + public class NearbyPlacesInfo { + List placeList; // List of nearby places + LatLng[] boundaryCoordinates; // Corners of nearby area + } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java deleted file mode 100644 index 2e677f33d..000000000 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java +++ /dev/null @@ -1,152 +0,0 @@ -package fr.free.nrw.commons.nearby; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.support.v4.app.FragmentActivity; -import android.support.v7.widget.PopupMenu; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import butterknife.Unbinder; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.ui.widget.OverlayDialog; -import fr.free.nrw.commons.utils.DialogUtil; - -public class NearbyInfoDialog extends OverlayDialog { - - private final static String ARG_TITLE = "placeTitle"; - private final static String ARG_DESC = "placeDesc"; - private final static String ARG_LATITUDE = "latitude"; - private final static String ARG_LONGITUDE = "longitude"; - private final static String ARG_SITE_LINK = "sitelink"; - - @BindView(R.id.link_preview_title) TextView placeTitle; - @BindView(R.id.link_preview_extract) TextView placeDescription; - @BindView(R.id.link_preview_go_button) TextView goToButton; - @BindView(R.id.link_preview_overflow_button) ImageView overflowButton; - - private Unbinder unbinder; - private LatLng location; - private Sitelinks sitelinks; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.dialog_nearby_info, container, false); - unbinder = ButterKnife.bind(this, view); - initUi(); - return view; - } - - private void initUi() { - Bundle bundle = getArguments(); - placeTitle.setText(bundle.getString(ARG_TITLE)); - placeDescription.setText(bundle.getString(ARG_DESC)); - location = new LatLng(bundle.getDouble(ARG_LATITUDE), bundle.getDouble(ARG_LONGITUDE), 0); - getArticleLink(bundle); - } - - private void getArticleLink(Bundle bundle) { - this.sitelinks = bundle.getParcelable(ARG_SITE_LINK); - - if (sitelinks == null || Uri.EMPTY.equals(sitelinks.getWikipediaLink())) { - goToButton.setVisibility(View.GONE); - } - - overflowButton.setVisibility(showMenu() ? View.VISIBLE : View.GONE); - - overflowButton.setOnClickListener(v -> popupMenuListener()); - } - - private void popupMenuListener() { - PopupMenu popupMenu = new PopupMenu(getActivity(), overflowButton); - popupMenu.inflate(R.menu.nearby_info_dialog_options); - - MenuItem commonsArticle = popupMenu.getMenu() - .findItem(R.id.nearby_info_menu_commons_article); - MenuItem wikiDataArticle = popupMenu.getMenu() - .findItem(R.id.nearby_info_menu_wikidata_article); - - commonsArticle.setEnabled(!sitelinks.getCommonsLink().equals(Uri.EMPTY)); - wikiDataArticle.setEnabled(!sitelinks.getWikidataLink().equals(Uri.EMPTY)); - - popupMenu.setOnMenuItemClickListener(menuListener); - popupMenu.show(); - } - - private boolean showMenu() { - return !sitelinks.getCommonsLink().equals(Uri.EMPTY) - || !sitelinks.getWikidataLink().equals(Uri.EMPTY); - } - - private final PopupMenu.OnMenuItemClickListener menuListener = new PopupMenu - .OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case R.id.nearby_info_menu_commons_article: - openWebView(sitelinks.getCommonsLink()); - return true; - case R.id.nearby_info_menu_wikidata_article: - openWebView(sitelinks.getWikidataLink()); - return true; - default: - break; - } - return false; - } - }; - - public static void showYourself(FragmentActivity fragmentActivity, Place place) { - NearbyInfoDialog mDialog = new NearbyInfoDialog(); - Bundle bundle = new Bundle(); - bundle.putString(ARG_TITLE, place.name); - bundle.putString(ARG_DESC, place.getDescription().getText()); - bundle.putDouble(ARG_LATITUDE, place.location.getLatitude()); - bundle.putDouble(ARG_LONGITUDE, place.location.getLongitude()); - bundle.putParcelable(ARG_SITE_LINK, place.siteLinks); - mDialog.setArguments(bundle); - DialogUtil.showSafely(fragmentActivity, mDialog); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - unbinder.unbind(); - } - - @OnClick(R.id.link_preview_directions_button) - void onDirectionsClick() { - //Open map app at given position - Uri gmmIntentUri = Uri.parse( - "geo:0,0?q=" + location.getLatitude() + "," + location.getLongitude()); - Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); - - if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) { - startActivity(mapIntent); - } - } - - @OnClick(R.id.link_preview_go_button) - void onReadArticleClick() { - openWebView(sitelinks.getWikipediaLink()); - } - - private void openWebView(Uri link) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, link); - startActivity(browserIntent); - } - - @OnClick(R.id.emptyLayout) - void onCloseClicked() { - dismissAllowingStateLoss(); - } -} 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 81bc496f2..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; @@ -11,17 +15,23 @@ import android.view.ViewGroup; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; +import com.pedrogomez.renderers.RVRendererAdapter; 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(); @@ -33,6 +43,7 @@ public class NearbyListFragment extends DaggerFragment { private NearbyAdapterFactory adapterFactory; private RecyclerView recyclerView; + private ContributionController controller; @Override public void onCreate(Bundle savedInstanceState) { @@ -40,15 +51,23 @@ 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, Bundle savedInstanceState) { Timber.d("NearbyListFragment created"); View view = inflater.inflate(R.layout.fragment_nearby, container, false); - recyclerView = (RecyclerView) view.findViewById(R.id.listView); + recyclerView = view.findViewById(R.id.listView); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - adapterFactory = new NearbyAdapterFactory(place -> NearbyInfoDialog.showYourself(getActivity(), place)); + + controller = new ContributionController(this); + adapterFactory = new NearbyAdapterFactory(this, controller); return view; } @@ -56,9 +75,19 @@ public class NearbyListFragment extends DaggerFragment { public void onViewCreated(View view, Bundle savedInstanceState) { // Check that this is the first time view is created, // to avoid double list when screen orientation changed + Bundle bundle = this.getArguments(); + recyclerView.setAdapter(adapterFactory.create(getPlaceListFromBundle(bundle))); + } + + public void updateNearbyListSignificantly() { + Bundle bundle = this.getArguments(); + adapterFactory.updateAdapterData(getPlaceListFromBundle(bundle), + (RVRendererAdapter) recyclerView.getAdapter()); + } + + private List getPlaceListFromBundle(Bundle bundle) { List placeList = Collections.emptyList(); - Bundle bundle = this.getArguments(); if (bundle != null) { String gsonPlaceList = bundle.getString("PlaceList", "[]"); placeList = gson.fromJson(gsonPlaceList, LIST_TYPE); @@ -69,6 +98,46 @@ public class NearbyListFragment extends DaggerFragment { placeList = NearbyController.loadAttractionsFromLocationToPlaces(curLatLng, placeList); } - recyclerView.setAdapter(adapterFactory.create(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 910c61328..7d0cda450 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 @@ -1,37 +1,118 @@ 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; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.BottomSheetBehavior; +import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.FloatingActionButton; +import android.view.Gravity; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.mapbox.mapboxsdk.Mapbox; +import com.mapbox.mapboxsdk.annotations.Icon; +import com.mapbox.mapboxsdk.annotations.IconFactory; +import com.mapbox.mapboxsdk.annotations.Marker; import com.mapbox.mapboxsdk.annotations.MarkerOptions; import com.mapbox.mapboxsdk.annotations.PolygonOptions; import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; import com.mapbox.mapboxsdk.constants.Style; import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.maps.MapView; import com.mapbox.mapboxsdk.maps.MapboxMap; import com.mapbox.mapboxsdk.maps.MapboxMapOptions; +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; import com.mapbox.services.android.telemetry.MapboxTelemetry; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.utils.UriDeserializer; +import javax.inject.Inject; +import javax.inject.Named; + +import dagger.android.support.DaggerFragment; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.contributions.ContributionController; +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 NearbyMapFragment extends DaggerFragment { -public class NearbyMapFragment extends android.support.v4.app.Fragment { private MapView mapView; private List baseMarkerOptions; private fr.free.nrw.commons.location.LatLng curLatLng; + public fr.free.nrw.commons.location.LatLng[] boundaryCoordinates; + + private View bottomSheetList; + private View bottomSheetDetails; + + private BottomSheetBehavior bottomSheetListBehavior; + private BottomSheetBehavior bottomSheetDetailsBehavior; + private LinearLayout wikipediaButton; + private LinearLayout wikidataButton; + private LinearLayout directionsButton; + private LinearLayout commonsButton; + private FloatingActionButton fabPlus; + private FloatingActionButton fabCamera; + private FloatingActionButton fabGallery; + private FloatingActionButton fabRecenter; + private View transparentView; + private TextView description; + private TextView title; + private TextView distance; + private ImageView icon; + + private TextView wikipediaButtonText; + private TextView wikidataButtonText; + private TextView commonsButtonText; + private TextView directionsButtonText; + + private boolean isFabOpen = false; + private Animation rotate_backward; + private Animation fab_close; + private Animation fab_open; + private Animation rotate_forward; + private ContributionController controller; + + private Place place; + private Marker selected; + private Marker currentLocationMarker; + private MapboxMap mapboxMap; + private PolygonOptions currentLocationPolygonOptions; + + private boolean isBottomListSheetExpanded; + private final double CAMERA_TARGET_SHIFT_FACTOR = 0.06; + + @Inject + @Named("prefs") + SharedPreferences prefs; + @Inject + @Named("direct_nearby_upload_prefs") + SharedPreferences directPrefs; public NearbyMapFragment() { } @@ -46,18 +127,24 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment { if (bundle != null) { String gsonPlaceList = bundle.getString("PlaceList"); String gsonLatLng = bundle.getString("CurLatLng"); - Type listType = new TypeToken>() {}.getType(); + Type listType = new TypeToken>() { + }.getType(); + String gsonBoundaryCoordinates = bundle.getString("BoundaryCoord"); List placeList = gson.fromJson(gsonPlaceList, listType); - Type curLatLngType = new TypeToken() {}.getType(); + Type curLatLngType = new TypeToken() { + }.getType(); + Type gsonBoundaryCoordinatesType = new TypeToken() {}.getType(); curLatLng = gson.fromJson(gsonLatLng, curLatLngType); baseMarkerOptions = NearbyController .loadAttractionsFromLocationToBaseMarkerOptions(curLatLng, placeList, getActivity()); + boundaryCoordinates = gson.fromJson(gsonBoundaryCoordinates, gsonBoundaryCoordinatesType); } Mapbox.getInstance(getActivity(), getString(R.string.mapbox_commons_app_token)); MapboxTelemetry.getInstance().setTelemetryEnabled(false); + setRetainInstance(true); } @Override @@ -73,9 +160,257 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment { return mapView; } + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + this.getView().setFocusableInTouchMode(true); + this.getView().requestFocus(); + this.getView().setOnKeyListener((v, keyCode, event) -> { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior + .STATE_EXPANDED) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + return true; + } else if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior + .STATE_COLLAPSED) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + mapView.getMapAsync(MapboxMap::deselectMarkers); + selected = null; + return true; + } + } + return false; + }); + } + + public void updateMapSlightly() { + // Get arguments from bundle for new location + Bundle bundle = this.getArguments(); + if (mapboxMap != null) { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Uri.class, new UriDeserializer()) + .create(); + if (bundle != null) { + String gsonLatLng = bundle.getString("CurLatLng"); + Type curLatLngType = new TypeToken() {}.getType(); + curLatLng = gson.fromJson(gsonLatLng, curLatLngType); + } + updateMapToTrackPosition(); + } + + } + + public void updateMapSignificantly() { + + Bundle bundle = this.getArguments(); + if (mapboxMap != null) { + if (bundle != null) { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Uri.class, new UriDeserializer()) + .create(); + + String gsonPlaceList = bundle.getString("PlaceList"); + String gsonLatLng = bundle.getString("CurLatLng"); + String gsonBoundaryCoordinates = bundle.getString("BoundaryCoord"); + Type listType = new TypeToken>() {}.getType(); + List placeList = gson.fromJson(gsonPlaceList, listType); + Type curLatLngType = new TypeToken() {}.getType(); + Type gsonBoundaryCoordinatesType = new TypeToken() {}.getType(); + curLatLng = gson.fromJson(gsonLatLng, curLatLngType); + baseMarkerOptions = NearbyController + .loadAttractionsFromLocationToBaseMarkerOptions(curLatLng, + placeList, + getActivity()); + boundaryCoordinates = gson.fromJson(gsonBoundaryCoordinates, gsonBoundaryCoordinatesType); + } + mapboxMap.clear(); + addCurrentLocationMarker(mapboxMap); + updateMapToTrackPosition(); + addNearbyMarkerstoMapBoxMap(); + } + } + + // Only update current position marker and camera view + private void updateMapToTrackPosition() { + + if (currentLocationMarker != null) { + LatLng curMapBoxLatLng = new LatLng(curLatLng.getLatitude(),curLatLng.getLongitude()); + ValueAnimator markerAnimator = ObjectAnimator.ofObject(currentLocationMarker, "position", + new LatLngEvaluator(), currentLocationMarker.getPosition(), + curMapBoxLatLng); + markerAnimator.setDuration(1000); + markerAnimator.start(); + + List circle = createCircleArray(curLatLng.getLatitude(), curLatLng.getLongitude(), + curLatLng.getAccuracy() * 2, 100); + if (currentLocationPolygonOptions != null){ + mapboxMap.removePolygon(currentLocationPolygonOptions.getPolygon()); + currentLocationPolygonOptions = new PolygonOptions() + .addAll(circle) + .strokeColor(Color.parseColor("#55000000")) + .fillColor(Color.parseColor("#11000000")); + mapboxMap.addPolygon(currentLocationPolygonOptions); + } + + // Make camera to follow user on location change + CameraPosition position = new CameraPosition.Builder() + .target(isBottomListSheetExpanded ? + new LatLng(curMapBoxLatLng.getLatitude()- CAMERA_TARGET_SHIFT_FACTOR, + curMapBoxLatLng.getLongitude()) + : curMapBoxLatLng ) // Sets the new camera position + .zoom(mapboxMap.getCameraPosition().zoom) // Same zoom level + .build(); + + mapboxMap.animateCamera(CameraUpdateFactory + .newCameraPosition(position), 1000); + + } + } + + private void updateMapCameraAccordingToBottomSheet(boolean isBottomListSheetExpanded) { + CameraPosition position; + this.isBottomListSheetExpanded = isBottomListSheetExpanded; + if (mapboxMap != null && curLatLng != null) { + if (isBottomListSheetExpanded) { + // Make camera to follow user on location change + position = new CameraPosition.Builder() + .target(new LatLng(curLatLng.getLatitude() - CAMERA_TARGET_SHIFT_FACTOR, + curLatLng.getLongitude())) // Sets the new camera target above + // current to make it visible when sheet is expanded + .zoom(11) // Same zoom level + .build(); + + } else { + // Make camera to follow user on location change + position = new CameraPosition.Builder() + .target(new LatLng(curLatLng.getLatitude(), + curLatLng.getLongitude())) // Sets the new camera target to curLatLng + .zoom(mapboxMap.getCameraPosition().zoom) // Same zoom level + .build(); + } + mapboxMap.animateCamera(CameraUpdateFactory + .newCameraPosition(position), 1000); + } + } + + private void initViews() { + bottomSheetList = getActivity().findViewById(R.id.bottom_sheet); + bottomSheetListBehavior = BottomSheetBehavior.from(bottomSheetList); + bottomSheetDetails = getActivity().findViewById(R.id.bottom_sheet_details); + bottomSheetDetailsBehavior = BottomSheetBehavior.from(bottomSheetDetails); + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetDetails.setVisibility(View.VISIBLE); + + fabPlus = getActivity().findViewById(R.id.fab_plus); + fabCamera = getActivity().findViewById(R.id.fab_camera); + fabGallery = getActivity().findViewById(R.id.fab_galery); + fabRecenter = getActivity().findViewById(R.id.fab_recenter); + + fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open); + fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close); + rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward); + rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward); + + transparentView = getActivity().findViewById(R.id.transparentView); + + description = getActivity().findViewById(R.id.description); + title = getActivity().findViewById(R.id.title); + distance = getActivity().findViewById(R.id.category); + icon = getActivity().findViewById(R.id.icon); + + wikidataButton = getActivity().findViewById(R.id.wikidataButton); + wikipediaButton = getActivity().findViewById(R.id.wikipediaButton); + directionsButton = getActivity().findViewById(R.id.directionsButton); + commonsButton = getActivity().findViewById(R.id.commonsButton); + + wikidataButtonText = getActivity().findViewById(R.id.wikidataButtonText); + wikipediaButtonText = getActivity().findViewById(R.id.wikipediaButtonText); + directionsButtonText = getActivity().findViewById(R.id.directionsButtonText); + commonsButtonText = getActivity().findViewById(R.id.commonsButtonText); + + } + + private void setListeners() { + fabPlus.setOnClickListener(view -> animateFAB(isFabOpen)); + + bottomSheetDetails.setOnClickListener(view -> { + if (bottomSheetDetailsBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + } else { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + }); + + fabRecenter.setOnClickListener(view -> { + if (curLatLng != null) { + mapView.getMapAsync(mapboxMap -> { + CameraPosition position = new CameraPosition.Builder() + .target(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude())) // Sets the new camera position + .zoom(11) // Sets the zoom + .build(); // Creates a CameraPosition from the builder + + mapboxMap.animateCamera(CameraUpdateFactory + .newCameraPosition(position), 1000); + + }); + } + }); + + bottomSheetDetailsBehavior.setBottomSheetCallback(new BottomSheetBehavior + .BottomSheetCallback() { + @Override + public void onStateChanged(@NonNull View bottomSheet, int newState) { + prepareViewsForSheetPosition(newState); + } + + @Override + public void onSlide(@NonNull View bottomSheet, float slideOffset) { + if (slideOffset >= 0) { + transparentView.setAlpha(slideOffset); + if (slideOffset == 1) { + transparentView.setClickable(true); + } else if (slideOffset == 0) { + transparentView.setClickable(false); + } + } + } + }); + + bottomSheetListBehavior.setBottomSheetCallback(new BottomSheetBehavior + .BottomSheetCallback() { + @Override + public void onStateChanged(@NonNull View bottomSheet, int newState) { + if (newState == BottomSheetBehavior.STATE_EXPANDED) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + updateMapCameraAccordingToBottomSheet(true); + } else { + updateMapCameraAccordingToBottomSheet(false); + } + } + + @Override + public void onSlide(@NonNull View bottomSheet, float slideOffset) { + + } + }); + + // Remove texts if it doesnt fit + if (wikipediaButtonText.getLineCount() > 1 + || wikidataButtonText.getLineCount() > 1 + || commonsButtonText.getLineCount() > 1 + || directionsButtonText.getLineCount() > 1) { + wikipediaButtonText.setVisibility(View.GONE); + wikidataButtonText.setVisibility(View.GONE); + commonsButtonText.setVisibility(View.GONE); + directionsButtonText.setVisibility(View.GONE); + } + } + 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) @@ -84,21 +419,13 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment { // create map mapView = new MapView(getActivity(), options); mapView.onCreate(savedInstanceState); - mapView.getMapAsync(mapboxMap -> { - mapboxMap.addMarkers(baseMarkerOptions); - - mapboxMap.setOnMarkerClickListener(marker -> { - if (marker instanceof NearbyMarker) { - NearbyMarker nearbyMarker = (NearbyMarker) marker; - Place place = nearbyMarker.getNearbyBaseMarker().getPlace(); - NearbyInfoDialog.showYourself(getActivity(), place); - } - return false; - }); - - addCurrentLocationMarker(mapboxMap); + mapView.getMapAsync(new OnMapReadyCallback() { + @Override + public void onMapReady(MapboxMap mapboxMap) { + NearbyMapFragment.this.mapboxMap = mapboxMap; + updateMapSignificantly(); + } }); - mapView.setStyleUrl("asset://mapstyle.json"); } @@ -107,23 +434,67 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment { * circle which uses the accuracy * 2, to draw a circle * which represents the user's position with an accuracy * of 95%. + * + * Should be called only on creation of mapboxMap, there + * is other method to update markers location with users + * move. */ private void addCurrentLocationMarker(MapboxMap mapboxMap) { - MarkerOptions currentLocationMarker = new MarkerOptions() + if (currentLocationMarker != null) { + currentLocationMarker.remove(); // Remove previous marker, we are not Hansel and Gretel + } + + Icon icon = IconFactory.getInstance(getContext()).fromResource(R.drawable.current_location_marker); + + MarkerOptions currentLocationMarkerOptions = new MarkerOptions() .position(new LatLng(curLatLng.getLatitude(), curLatLng.getLongitude())); - mapboxMap.addMarker(currentLocationMarker); + currentLocationMarkerOptions.setIcon(icon); // Set custom icon + + currentLocationMarker = mapboxMap.addMarker(currentLocationMarkerOptions); List circle = createCircleArray(curLatLng.getLatitude(), curLatLng.getLongitude(), curLatLng.getAccuracy() * 2, 100); - mapboxMap.addPolygon( - new PolygonOptions() - .addAll(circle) - .strokeColor(Color.parseColor("#55000000")) - .fillColor(Color.parseColor("#11000000")) - ); + currentLocationPolygonOptions = new PolygonOptions() + .addAll(circle) + .strokeColor(Color.parseColor("#55000000")) + .fillColor(Color.parseColor("#11000000")); + mapboxMap.addPolygon(currentLocationPolygonOptions); } + private void addNearbyMarkerstoMapBoxMap() { + + mapboxMap.addMarkers(baseMarkerOptions); + mapboxMap.setOnInfoWindowCloseListener(marker -> { + if (marker == selected) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } + }); + mapView.getMapAsync(mapboxMap -> { + mapboxMap.addMarkers(baseMarkerOptions); + fabRecenter.setVisibility(View.VISIBLE); + mapboxMap.setOnInfoWindowCloseListener(marker -> { + if (marker == selected) { + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } + }); + + mapboxMap.setOnMarkerClickListener(marker -> { + if (marker instanceof NearbyMarker) { + this.selected = marker; + NearbyMarker nearbyMarker = (NearbyMarker) marker; + Place place = nearbyMarker.getNearbyBaseMarker().getPlace(); + passInfoToSheet(place); + bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + return false; + }); + + }); + } + + /** * Creates a series of points that create a circle on the map. * Takes the center latitude, center longitude of the circle, @@ -145,10 +516,230 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment { double nodeLatitude = centerLat + radiusLat * Math.sin(theta); circle.add(new LatLng(nodeLatitude, nodeLongitude)); } - return circle; } + public void prepareViewsForSheetPosition(int bottomSheetState) { + + switch (bottomSheetState) { + case (BottomSheetBehavior.STATE_COLLAPSED): + closeFabs(isFabOpen); + if (!fabPlus.isShown()) showFAB(); + this.getView().requestFocus(); + break; + case (BottomSheetBehavior.STATE_EXPANDED): + this.getView().requestFocus(); + break; + case (BottomSheetBehavior.STATE_HIDDEN): + mapView.getMapAsync(MapboxMap::deselectMarkers); + transparentView.setClickable(false); + transparentView.setAlpha(0); + closeFabs(isFabOpen); + hideFAB(); + this.getView().requestFocus(); + break; + } + } + + private void hideFAB() { + + removeAnchorFromFABs(fabPlus); + fabPlus.hide(); + + removeAnchorFromFABs(fabCamera); + fabCamera.hide(); + + removeAnchorFromFABs(fabGallery); + fabGallery.hide(); + + } + + /* + * We are not able to hide FABs without removing anchors, this method removes anchors + * */ + private void removeAnchorFromFABs(FloatingActionButton floatingActionButton) { + //get rid of anchors + //Somehow this was the only way https://stackoverflow.com/questions/32732932 + // /floatingactionbutton-visible-for-sometime-even-if-visibility-is-set-to-gone + CoordinatorLayout.LayoutParams param = (CoordinatorLayout.LayoutParams) floatingActionButton + .getLayoutParams(); + param.setAnchorId(View.NO_ID); + // If we don't set them to zero, then they become visible for a moment on upper left side + param.width = 0; + param.height = 0; + floatingActionButton.setLayoutParams(param); + } + + private void showFAB() { + + addAnchorToBigFABs(fabPlus, getActivity().findViewById(R.id.bottom_sheet_details).getId()); + fabPlus.show(); + + addAnchorToSmallFABs(fabGallery, getActivity().findViewById(R.id.empty_view).getId()); + + addAnchorToSmallFABs(fabCamera, getActivity().findViewById(R.id.empty_view1).getId()); + + } + + + /* + * Add amnchors back before making them visible again. + * */ + private void addAnchorToBigFABs(FloatingActionButton floatingActionButton, int anchorID) { + CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams + (ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); + params.setAnchorId(anchorID); + params.anchorGravity = Gravity.TOP|Gravity.RIGHT|Gravity.END; + floatingActionButton.setLayoutParams(params); + } + + /* + * Add amnchors back before making them visible again. Big and small fabs have different anchor + * gravities, therefore the are two methods. + * */ + private void addAnchorToSmallFABs(FloatingActionButton floatingActionButton, int anchorID) { + CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams + (ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); + params.setAnchorId(anchorID); + params.anchorGravity = Gravity.CENTER_HORIZONTAL; + floatingActionButton.setLayoutParams(params); + } + + private void passInfoToSheet(Place place) { + this.place = place; + wikipediaButton.setEnabled(place.hasWikipediaLink()); + wikipediaButton.setOnClickListener(view -> openWebView(place.siteLinks.getWikipediaLink())); + + wikidataButton.setEnabled(place.hasWikidataLink()); + wikidataButton.setOnClickListener(view -> openWebView(place.siteLinks.getWikidataLink())); + + directionsButton.setOnClickListener(view -> { + //Open map app at given position + Intent mapIntent = new Intent(Intent.ACTION_VIEW, place.location.getGmmIntentUri()); + if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) { + startActivity(mapIntent); + } + }); + + commonsButton.setEnabled(place.hasCommonsLink()); + commonsButton.setOnClickListener(view -> openWebView(place.siteLinks.getCommonsLink())); + + 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()); + + fabCamera.setOnClickListener(view -> { + if (fabCamera.isShown()) { + Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); + controller = new ContributionController(this); + + DirectUpload directUpload = new DirectUpload(this, controller); + storeSharedPrefs(); + directUpload.initiateCameraUpload(); + } + }); + + fabGallery.setOnClickListener(view -> { + if (fabGallery.isShown()) { + Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); + controller = new ContributionController(this); + + DirectUpload directUpload = new DirectUpload(this, controller); + storeSharedPrefs(); + directUpload.initiateGalleryUpload(); + + //TODO: App crashes after image upload completes + //TODO: Handle onRequestPermissionsResult + } + }); + } + + void storeSharedPrefs() { + SharedPreferences.Editor editor = directPrefs.edit(); + editor.putString("Title", place.getName()); + editor.putString("Desc", place.getLongDescription()); + editor.putString("Category", place.getCategory()); + editor.apply(); + } + + @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); + } + } + + private void openWebView(Uri link) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, link); + startActivity(browserIntent); + } + + private void animateFAB(boolean isFabOpen) { + this.isFabOpen = !isFabOpen; + if (fabPlus.isShown()){ + if (isFabOpen) { + fabPlus.startAnimation(rotate_backward); + fabCamera.startAnimation(fab_close); + fabGallery.startAnimation(fab_close); + fabCamera.hide(); + fabGallery.hide(); + } else { + fabPlus.startAnimation(rotate_forward); + fabCamera.startAnimation(fab_open); + fabGallery.startAnimation(fab_open); + fabCamera.show(); + fabGallery.show(); + } + this.isFabOpen=!isFabOpen; + } + } + + private void closeFabs ( boolean isFabOpen){ + if (isFabOpen) { + fabPlus.startAnimation(rotate_backward); + fabCamera.startAnimation(fab_close); + fabGallery.startAnimation(fab_close); + fabCamera.hide(); + fabGallery.hide(); + this.isFabOpen = !isFabOpen; + } + } + @Override public void onStart() { if (mapView != null) { @@ -167,10 +758,14 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment { @Override public void onResume() { + super.onResume(); if (mapView != null) { mapView.onResume(); } - super.onResume(); + initViews(); + setListeners(); + transparentView.setClickable(false); + transparentView.setAlpha(0); } @Override @@ -188,4 +783,19 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment { } super.onDestroyView(); } + + private static class LatLngEvaluator implements TypeEvaluator { + // Method is used to interpolate the marker animation. + private LatLng latLng = new LatLng(); + + @Override + public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) { + latLng.setLatitude(startValue.getLatitude() + + ((endValue.getLatitude() - startValue.getLatitude()) * fraction)); + latLng.setLongitude(startValue.getLongitude() + + ((endValue.getLongitude() - startValue.getLongitude()) * fraction)); + return latLng; + } + } } + 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 de14a2ef8..a2f4b2352 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 @@ -1,7 +1,6 @@ package fr.free.nrw.commons.nearby; import android.net.Uri; -import android.os.StrictMode; import java.io.BufferedReader; import java.io.IOException; @@ -9,6 +8,7 @@ import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -30,11 +30,10 @@ public class NearbyPlaces { private static final Uri WIKIDATA_QUERY_UI_URL = Uri.parse("https://query.wikidata.org/"); private final String wikidataQuery; private double radius = INITIAL_RADIUS; - private List places; 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); @@ -102,13 +101,17 @@ public class NearbyPlaces { } String[] fields = line.split("\t"); + Timber.v("Fields: " + Arrays.toString(fields)); String point = fields[0]; + String wikiDataLink = Utils.stripLocalizedString(fields[1]); String name = Utils.stripLocalizedString(fields[2]); String type = Utils.stripLocalizedString(fields[4]); + String icon = fields[5]; String wikipediaSitelink = Utils.stripLocalizedString(fields[7]); String commonsSitelink = Utils.stripLocalizedString(fields[8]); - String wikiDataLink = Utils.stripLocalizedString(fields[1]); - String icon = fields[5]; + String category = Utils.stripLocalizedString(fields[9]); + + Timber.v("Name: " + name + ", type: " + type + ", category: " + category + ", wikipediaSitelink: " + wikipediaSitelink + ", commonsSitelink: " + commonsSitelink); double latitude; double longitude; @@ -126,10 +129,11 @@ public class NearbyPlaces { places.add(new Place( name, - Place.Description.fromText(type), // list + Place.Label.fromText(type), // list type, // details Uri.parse(icon), new LatLng(latitude, longitude, 0), + category, new Sitelinks.Builder() .setWikipediaLink(wikipediaSitelink) .setCommonsLink(commonsSitelink) @@ -141,66 +145,4 @@ public class NearbyPlaces { return places; } - - List getFromWikiNeedsPictures() { - if (places != null) { - return places; - } else { - try { - places = new ArrayList<>(); - StrictMode.ThreadPolicy policy - = new StrictMode.ThreadPolicy.Builder().permitAll().build(); - StrictMode.setThreadPolicy(policy); - - URL file = new URL("https://tools.wmflabs.org/wiki-needs-pictures/data/data.csv"); - - BufferedReader in = new BufferedReader(new InputStreamReader(file.openStream())); - - boolean firstLine = true; - String line; - Timber.d("Reading from CSV file..."); - - while ((line = in.readLine()) != null) { - - // Skip CSV header. - if (firstLine) { - firstLine = false; - continue; - } - - String[] fields = line.split(","); - String name = Utils.stripLocalizedString(fields[0]); - - double latitude; - double longitude; - try { - latitude = Double.parseDouble(fields[1]); - } catch (NumberFormatException e) { - latitude = 0; - } - try { - longitude = Double.parseDouble(fields[2]); - } catch (NumberFormatException e) { - longitude = 0; - } - - String type = fields[3]; - - places.add(new Place( - name, - Place.Description.fromText(type), // list - type, // details - null, - new LatLng(latitude, longitude, 0), - new Sitelinks.Builder().build() - )); - } - in.close(); - - } catch (IOException e) { - Timber.d(e.toString()); - } - } - return places; - } } 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/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java index 428fcf6de..9c5138245 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java @@ -13,10 +13,11 @@ import fr.free.nrw.commons.location.LatLng; public class Place { public final String name; - private final Description description; + private final Label label; private final String longDescription; private final Uri secondaryImageUrl; public final LatLng location; + private final String category; public Bitmap image; public Bitmap secondaryImage; @@ -24,24 +25,43 @@ public class Place { public final Sitelinks siteLinks; - public Place(String name, Description description, String longDescription, - Uri secondaryImageUrl, LatLng location, Sitelinks siteLinks) { + public Place(String name, Label label, String longDescription, + Uri secondaryImageUrl, LatLng location, String category, Sitelinks siteLinks) { this.name = name; - this.description = description; + this.label = label; this.longDescription = longDescription; this.secondaryImageUrl = secondaryImageUrl; this.location = location; + this.category = category; this.siteLinks = siteLinks; } - public Description getDescription() { - return description; + public String getName() { return name; } + + public Label getLabel() { + return label; } + public String getLongDescription() { return longDescription; } + + public String getCategory() {return category; } + public void setDistance(String distance) { this.distance = distance; } + public boolean hasWikipediaLink() { + return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikipediaLink())); + } + + public boolean hasWikidataLink() { + return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikidataLink())); + } + + public boolean hasCommonsLink() { + return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getCommonsLink())); + } + @Override public boolean equals(Object o) { if (o instanceof Place) { @@ -67,10 +87,8 @@ public class Place { * Most common types of desc: building, house, cottage, farmhouse, * village, civil parish, church, railway station, * gatehouse, milestone, inn, secondary school, hotel - * - * TODO Give a more accurate class name (see issue #742). */ - public enum Description { + public enum Label { BUILDING("building", R.drawable.round_icon_generic_building), HOUSE("house", R.drawable.round_icon_house), @@ -95,19 +113,19 @@ public class Place { WATERFALL("waterfall", R.drawable.round_icon_waterfall), UNKNOWN("?", R.drawable.round_icon_unknown); - private static final Map TEXT_TO_DESCRIPTION - = new HashMap<>(Description.values().length); + private static final Map TEXT_TO_DESCRIPTION + = new HashMap<>(Label.values().length); static { - for (Description description : values()) { - TEXT_TO_DESCRIPTION.put(description.text, description); + for (Label label : values()) { + TEXT_TO_DESCRIPTION.put(label.text, label); } } private final String text; @DrawableRes private final int icon; - Description(String text, @DrawableRes int icon) { + Label(String text, @DrawableRes int icon) { this.text = text; this.icon = icon; } @@ -121,9 +139,9 @@ public class Place { return icon; } - public static Description fromText(String text) { - Description description = TEXT_TO_DESCRIPTION.get(text); - return description == null ? UNKNOWN : description; + public static Label fromText(String text) { + Label label = TEXT_TO_DESCRIPTION.get(text); + return label == null ? UNKNOWN : label; } } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java index f4c8a5d61..5216dc36d 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,32 +1,79 @@ package fr.free.nrw.commons.nearby; -import android.support.annotation.NonNull; +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; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.TextView; 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 fr.free.nrw.commons.di.ApplicationlessInjection; +import timber.log.Timber; + +public class PlaceRenderer extends Renderer { -class PlaceRenderer extends Renderer { @BindView(R.id.tvName) TextView tvName; @BindView(R.id.tvDesc) TextView tvDesc; @BindView(R.id.distance) TextView distance; @BindView(R.id.icon) ImageView icon; - private final PlaceClickedListener listener; + @BindView(R.id.buttonLayout) LinearLayout buttonLayout; + @BindView(R.id.cameraButton) LinearLayout cameraButton; - PlaceRenderer(@NonNull PlaceClickedListener listener) { - this.listener = listener; + @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.galleryButtonText) TextView galleryButtonText; + + @BindView(R.id.directionsButtonText) TextView directionsButtonText; + @BindView(R.id.iconOverflowText) TextView iconOverflowText; + + private View view; + private static ArrayList openedItems; + private Place place; + + private Fragment fragment; + private ContributionController controller; + + + @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<>(); } @Override protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) { - return layoutInflater.inflate(R.layout.item_place, viewGroup, false); + view = layoutInflater.inflate(R.layout.item_place, viewGroup, false); + return view; } @Override @@ -36,23 +83,129 @@ class PlaceRenderer extends Renderer { @Override protected void hookListeners(View view) { - view.setOnClickListener(v -> listener.placeClicked(getContent())); + + final View.OnClickListener listener = view12 -> { + Log.d("Renderer", "clicked"); + TransitionManager.beginDelayedTransition(buttonLayout); + + if(buttonLayout.isShown()){ + closeLayout(buttonLayout); + }else { + openLayout(buttonLayout); + } + + }; + view.setOnClickListener(listener); + view.requestFocus(); + view.setOnFocusChangeListener((view1, hasFocus) -> { + if (!hasFocus && buttonLayout.isShown()) { + closeLayout(buttonLayout); + } else if (hasFocus && !buttonLayout.isShown()) { + listener.onClick(view1); + } + }); + + 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.putString("Category", place.getCategory()); + editor.apply(); + } + + private void closeLayout(LinearLayout buttonLayout){ + buttonLayout.setVisibility(View.GONE); + } + + private void openLayout(LinearLayout buttonLayout){ + buttonLayout.setVisibility(View.VISIBLE); } @Override public void render() { - Place place = getContent(); + ApplicationlessInjection.getInstance(getContext().getApplicationContext()) + .getCommonsApplicationComponent().inject(this); + place = getContent(); tvName.setText(place.name); - String descriptionText = place.getDescription().getText(); + String descriptionText = place.getLongDescription(); if (descriptionText.equals("?")) { descriptionText = getContext().getString(R.string.no_description_found); + tvDesc.setVisibility(View.INVISIBLE); } tvDesc.setText(descriptionText); distance.setText(place.distance); - icon.setImageResource(place.getDescription().getIcon()); + icon.setImageResource(place.getLabel().getIcon()); + + directionsButton.setOnClickListener(view -> { + //Open map app at given position + Intent mapIntent = new Intent(Intent.ACTION_VIEW, place.location.getGmmIntentUri()); + if (mapIntent.resolveActivity(view.getContext().getPackageManager()) != null) { + view.getContext().startActivity(mapIntent); + } + }); + + iconOverflow.setVisibility(showMenu() ? View.VISIBLE : View.GONE); + iconOverflow.setOnClickListener(v -> popupMenuListener()); + } - interface PlaceClickedListener { - void placeClicked(Place place); + private void popupMenuListener() { + PopupMenu popupMenu = new PopupMenu(view.getContext(), iconOverflow); + popupMenu.inflate(R.menu.nearby_info_dialog_options); + + MenuItem commonsArticle = popupMenu.getMenu() + .findItem(R.id.nearby_info_menu_commons_article); + MenuItem wikiDataArticle = popupMenu.getMenu() + .findItem(R.id.nearby_info_menu_wikidata_article); + MenuItem wikipediaArticle = popupMenu.getMenu() + .findItem(R.id.nearby_info_menu_wikipedia_article); + + commonsArticle.setEnabled(place.hasCommonsLink()); + wikiDataArticle.setEnabled(place.hasWikidataLink()); + wikipediaArticle.setEnabled(place.hasWikipediaLink()); + + popupMenu.setOnMenuItemClickListener(item -> { + switch (item.getItemId()) { + case R.id.nearby_info_menu_commons_article: + openWebView(place.siteLinks.getCommonsLink()); + return true; + case R.id.nearby_info_menu_wikidata_article: + openWebView(place.siteLinks.getWikidataLink()); + return true; + case R.id.nearby_info_menu_wikipedia_article: + openWebView(place.siteLinks.getWikipediaLink()); + return true; + default: + break; + } + return false; + }); + popupMenu.show(); } -} + + private void openWebView(Uri link) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, link); + view.getContext().startActivity(browserIntent); + } + + private boolean showMenu() { + return place.hasCommonsLink() || place.hasWikidataLink(); + } + +} \ No newline at end of file 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 index cb1aa62b6..e4efbff6c 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/Notification.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/Notification.java @@ -7,20 +7,15 @@ package fr.free.nrw.commons.notification; public class Notification { public NotificationType notificationType; public String notificationText; + public String date; + public String description; + public String link; - - Notification (NotificationType notificationType, String notificationText) { + public Notification(NotificationType notificationType, String notificationText, String date, String description, String link) { this.notificationType = notificationType; this.notificationText = notificationText; - } - - - public enum NotificationType { - /* Added for test purposes, needs to be rescheduled after implementing - fetching notifications from server */ - edit, - mention, - message, - block; + 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 index 9da69e0eb..d6fcd2685 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -1,15 +1,32 @@ package fr.free.nrw.commons.notification; +import android.annotation.SuppressLint; +import android.app.FragmentManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; -import android.support.annotation.Nullable; +import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.widget.Toast; + +import com.pedrogomez.renderers.RVRendererAdapter; + +import java.util.List; + +import javax.inject.Inject; import butterknife.BindView; import butterknife.ButterKnife; -import butterknife.Optional; 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; + +import static android.widget.Toast.LENGTH_SHORT; /** * Created by root on 18.12.2017. @@ -18,33 +35,83 @@ import fr.free.nrw.commons.theme.NavigationBaseActivity; public class NotificationActivity extends NavigationBaseActivity { NotificationAdapterFactory notificationAdapterFactory; - @Nullable @BindView(R.id.listView) RecyclerView recyclerView; + @Inject NotificationController controller; + + private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment"; + private NotificationWorkerFragment mNotificationWorkerFragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_notification); ButterKnife.bind(this); + mNotificationWorkerFragment = (NotificationWorkerFragment) getFragmentManager() + .findFragmentByTag(TAG_NOTIFICATION_WORKER_FRAGMENT); initListView(); - addNotifications(); initDrawer(); } private void initListView() { - recyclerView = findViewById(R.id.listView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); - notificationAdapterFactory = new NotificationAdapterFactory(new NotificationRenderer.NotificationClicked() { - @Override - public void notificationClicked(Notification notification) { - - } - }); + DividerItemDecoration itemDecor = new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL); + recyclerView.addItemDecoration(itemDecor); + addNotifications(); } + @SuppressLint("CheckResult") private void addNotifications() { + Timber.d("Add notifications"); - recyclerView.setAdapter(notificationAdapterFactory.create(NotificationController.loadNotifications())); + if(mNotificationWorkerFragment == null){ + Observable.fromCallable(() -> controller.getNotifications()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(notificationList -> { + Timber.d("Number of notifications is %d", notificationList.size()); + initializeAndSetNotificationList(notificationList); + setAdapter(notificationList); + }, throwable -> Timber.e(throwable, "Error occurred while loading notifications")); + } else { + setAdapter(mNotificationWorkerFragment.getNotificationList()); + } } -} + + private void handleUrl(String url) { + if (url == null || url.equals("")) { + return; + } + Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + //check if web browser available + if(browser.resolveActivity(this.getPackageManager()) != null){ + startActivity(browser); + } else { + Toast toast = Toast.makeText(this, getString(R.string.no_web_browser), LENGTH_SHORT); + toast.show(); + } + } + + 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); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + context.startActivity(intent); + } + + private void initializeAndSetNotificationList(List notificationList){ + FragmentManager fm = getFragmentManager(); + mNotificationWorkerFragment = new NotificationWorkerFragment(); + fm.beginTransaction().add(mNotificationWorkerFragment, TAG_NOTIFICATION_WORKER_FRAGMENT) + .commit(); + mNotificationWorkerFragment.setNotificationList(notificationList); + } +} \ No newline at end of file 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 index 84f5c15d8..b22bafbb5 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java @@ -1,23 +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 { - public static List loadNotifications() { - List notifications = new ArrayList<>(); - notifications.add(new Notification(Notification.NotificationType.message, "notification 1")); - notifications.add(new Notification(Notification.NotificationType.edit, "notification 2")); - notifications.add(new Notification(Notification.NotificationType.mention, "notification 3")); - notifications.add(new Notification(Notification.NotificationType.message, "notification 4")); - notifications.add(new Notification(Notification.NotificationType.edit, "notification 5")); - notifications.add(new Notification(Notification.NotificationType.mention, "notification 6")); - notifications.add(new Notification(Notification.NotificationType.message, "notification 7")); - return notifications; + 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 index 36272d4a2..a02b9eff4 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java @@ -1,11 +1,13 @@ package fr.free.nrw.commons.notification; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import com.borjabravo.readmoretextview.ReadMoreTextView; import com.pedrogomez.renderers.Renderer; import butterknife.BindView; @@ -17,8 +19,8 @@ import fr.free.nrw.commons.R; */ public class NotificationRenderer extends Renderer { - @BindView(R.id.title) TextView title; - @BindView(R.id.description) TextView description; + @BindView(R.id.title) ReadMoreTextView title; + @BindView(R.id.description) ReadMoreTextView description; @BindView(R.id.time) TextView time; @BindView(R.id.icon) ImageView icon; private NotificationClicked listener; @@ -45,23 +47,21 @@ public class NotificationRenderer extends Renderer { @Override public void render() { - Notification notification = getContent(); - title.setText(notification.notificationText); - time.setText("3d"); - description.setText("Example notification description"); - switch (notification.notificationType) { - case edit: - icon.setImageResource(R.drawable.ic_edit_black_24dp); - break; - case message: - icon.setImageResource(R.drawable.ic_message_black_24dp); - break; - case mention: - icon.setImageResource(R.drawable.ic_chat_bubble_black_24px); - break; - default: - icon.setImageResource(R.drawable.round_icon_unknown); - } + Notification notification = getContent(); + StringBuilder str = new StringBuilder(notification.notificationText); + str.append(" " ); + title.setText(str); + time.setText(notification.date); + StringBuilder desc = new StringBuilder(notification.description); + desc.append(" "); + description.setText(desc); + 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{ 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/notification/NotificationWorkerFragment.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java new file mode 100644 index 000000000..bb85fc696 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.notification; + +import android.app.Fragment; +import android.os.Bundle; +import android.support.annotation.Nullable; + +import java.util.List; + +/** + * Created by knightshade on 25/2/18. + */ + +public class NotificationWorkerFragment extends Fragment { + private List notificationList; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + public void setNotificationList(List notificationList){ + this.notificationList = notificationList; + } + + public List getNotificationList(){ + return notificationList; + } +} 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..741905e30 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 @@ -3,14 +3,17 @@ package fr.free.nrw.commons.settings; import android.Manifest; import android.app.AlertDialog; import android.content.ActivityNotFoundException; +import android.content.ComponentName; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.preference.CheckBoxPreference; +import android.preference.SwitchPreference; import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; @@ -21,15 +24,17 @@ import android.support.v4.content.FileProvider; import android.widget.Toast; import java.io.File; +import java.util.ArrayList; +import java.util.List; 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 +45,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); @@ -56,7 +64,7 @@ public class SettingsFragment extends PreferenceFragment { return true; }); - CheckBoxPreference themePreference = (CheckBoxPreference) findPreference("theme"); + SwitchPreference themePreference = (SwitchPreference) findPreference("theme"); themePreference.setOnPreferenceChangeListener((preference, newValue) -> { getActivity().recreate(); return true; @@ -67,7 +75,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 +94,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; @@ -133,19 +146,24 @@ public class SettingsFragment extends PreferenceFragment { appLogsFile ); - Intent feedbackIntent = new Intent(Intent.ACTION_SEND); - feedbackIntent.setType("message/rfc822"); - feedbackIntent.putExtra(Intent.EXTRA_EMAIL, - new String[]{CommonsApplication.LOGS_PRIVATE_EMAIL}); - feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, - String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT, - BuildConfig.VERSION_NAME)); - feedbackIntent.putExtra(Intent.EXTRA_STREAM,appLogsFilePath); + //initialize the emailSelectorIntent + Intent emailSelectorIntent = new Intent(Intent.ACTION_SENDTO); + emailSelectorIntent.setData(Uri.parse("mailto:")); + //initialize the emailIntent + final Intent emailIntent = new Intent(Intent.ACTION_SEND); + emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{CommonsApplication.FEEDBACK_EMAIL}); + emailIntent.putExtra(Intent.EXTRA_SUBJECT, String.format(CommonsApplication.FEEDBACK_EMAIL_SUBJECT, BuildConfig.VERSION_NAME)); + emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + emailIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + emailIntent.setSelector( emailSelectorIntent ); + //adding the attachment to the intent + emailIntent.putExtra(Intent.EXTRA_STREAM, appLogsFilePath); try { - startActivity(feedbackIntent); + startActivity(Intent.createChooser(emailIntent, "Send mail..")); } catch (ActivityNotFoundException e) { Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show(); } } + } diff --git a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java index 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 dd8e3017c..acd9b7646 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; @@ -22,6 +23,7 @@ import fr.free.nrw.commons.AboutActivity; 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.WelcomeActivity; import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.LoginActivity; @@ -87,8 +89,11 @@ public abstract class NavigationBaseActivity extends BaseActivity private void setDrawerPaneWidth() { ViewGroup.LayoutParams params = navigationView.getLayoutParams(); - // set width to lowerBound of 80% of the screen size - params.width = (getResources().getDisplayMetrics().widthPixels * 70) / 100; + // set width to lowerBound of 70% of the screen size in portrait mode + // set width to lowerBound of 50% of the screen size in landscape mode + int percentageWidth = getResources().getInteger(R.integer.drawer_width); + + params.width = (getResources().getDisplayMetrics().widthPixels * percentageWidth) / 100; navigationView.setLayoutParams(params); } @@ -120,8 +125,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, @@ -147,7 +153,7 @@ public abstract class NavigationBaseActivity extends BaseActivity return true; case R.id.action_notifications: drawerLayout.closeDrawer(navigationView); - startActivityWithFlags(this, NotificationActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + NotificationActivity.startYourself(this); return true; case R.id.action_featured_images: drawerLayout.closeDrawer(navigationView); 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/ExistingFileAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java index fee0765a4..f74c40867 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ExistingFileAsync.java @@ -1,11 +1,13 @@ package fr.free.nrw.commons.upload; +import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.support.v7.app.AlertDialog; import java.io.IOException; +import java.lang.ref.WeakReference; import fr.free.nrw.commons.R; import fr.free.nrw.commons.contributions.ContributionsActivity; @@ -28,12 +30,14 @@ public class ExistingFileAsync extends AsyncTask { DUPLICATE_CANCELLED } + private final WeakReference activity; private final MediaWikiApi api; private final String fileSha1; - private final Context context; + private final WeakReference context; private final Callback callback; - public ExistingFileAsync(String fileSha1, Context context, Callback callback, MediaWikiApi mwApi) { + public ExistingFileAsync(WeakReference activity, String fileSha1, WeakReference context, Callback callback, MediaWikiApi mwApi) { + this.activity = activity; this.fileSha1 = fileSha1; this.context = context; this.callback = callback; @@ -69,19 +73,21 @@ public class ExistingFileAsync extends AsyncTask { // If file exists, display warning to user. // Use soft warning for now (user able to choose to proceed) until have determined that implementation works without bugs if (fileExists) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); + AlertDialog.Builder builder = new AlertDialog.Builder(context.get()); builder.setMessage(R.string.file_exists) .setTitle(R.string.warning); builder.setPositiveButton(R.string.no, (dialog, id) -> { //Go back to ContributionsActivity - Intent intent = new Intent(context, ContributionsActivity.class); - context.startActivity(intent); + Intent intent = new Intent(context.get(), ContributionsActivity.class); + context.get().startActivity(intent); callback.onResult(Result.DUPLICATE_CANCELLED); }); builder.setNegativeButton(R.string.yes, (dialog, id) -> callback.onResult(Result.DUPLICATE_PROCEED)); AlertDialog dialog = builder.create(); - dialog.show(); + if (!activity.get().isFinishing()) { + dialog.show(); + } } else { callback.onResult(Result.NO_DUPLICATE); } 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 0f0e742cc..5525f1ba7 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; @@ -52,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; @@ -63,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); @@ -166,19 +177,18 @@ public class MultipleShareActivity extends AuthenticatedActivity @Override public void onCategoriesSave(List categories) { if (categories.size() > 0) { - ModifierSequenceDao dao = new ModifierSequenceDao(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")); - dao.save(categoriesSequence); + 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 0992cbb31..f5e1820b8 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 @@ -1,7 +1,10 @@ package fr.free.nrw.commons.upload; import android.Manifest; +import android.app.Activity; import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; @@ -16,7 +19,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; @@ -29,6 +34,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.lang.ref.WeakReference; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -46,11 +52,14 @@ 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; + +import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; @@ -61,10 +70,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; @@ -72,11 +81,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; @@ -88,6 +105,7 @@ public class ShareActivity private boolean cacheFound; private GPSExtractor imageObj; + private GPSExtractor tempImageObj; private String decimalCoords; private boolean useNewPermissions = false; @@ -99,6 +117,9 @@ public class ShareActivity private Snackbar snackbar; private boolean duplicateCheckPassed = false; + private boolean haveCheckedForOtherImages = false; + private boolean isNearbyUpload = false; + /** * Called when user taps the submit button. */ @@ -166,13 +187,12 @@ public class ShareActivity categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{}))); categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized")); - ModifierSequenceDao dao = new ModifierSequenceDao(getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY)); - dao.save(categoriesSequence); + 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(); } @@ -197,6 +217,10 @@ public class ShareActivity finish(); } + protected boolean isNearbyUpload() { + return isNearbyUpload; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -216,13 +240,17 @@ 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); } else { source = Contribution.SOURCE_EXTERNAL; } + if (intent.hasExtra("isDirectUpload")) { + Timber.d("This was initiated by a direct upload from Nearby"); + isNearbyUpload = true; + } mimeType = intent.getType(); } @@ -278,7 +306,7 @@ public class ShareActivity REQUEST_PERM_ON_CREATE_LOCATION); } } - performPreuploadProcessingOfFile(); + performPreUploadProcessingOfFile(); SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView"); @@ -302,7 +330,7 @@ public class ShareActivity && grantResults[0] == PackageManager.PERMISSION_GRANTED) { backgroundImageView.setImageURI(mediaUri); storagePermitted = true; - performPreuploadProcessingOfFile(); + performPreUploadProcessingOfFile(); } return; } @@ -310,7 +338,7 @@ public class ShareActivity if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { locationPermitted = true; - performPreuploadProcessingOfFile(); + performPreUploadProcessingOfFile(); } return; } @@ -319,12 +347,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; } @@ -335,7 +363,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(); @@ -346,7 +374,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 @@ -357,11 +385,21 @@ public class ShareActivity Timber.d("File SHA1 is: %s", fileSHA1); ExistingFileAsync fileAsyncTask = - new ExistingFileAsync(fileSHA1, this, result -> { + new ExistingFileAsync(new WeakReference(this), fileSHA1, new WeakReference(this), result -> { 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: "); @@ -375,6 +413,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) { @@ -452,13 +521,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. @@ -466,6 +615,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) { @@ -489,7 +639,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 259f38c36..0fa98e530 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java @@ -1,5 +1,8 @@ package fr.free.nrw.commons.upload; +import android.annotation.SuppressLint; +import android.app.Activity; + import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -8,9 +11,11 @@ import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; 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; @@ -25,6 +30,7 @@ import android.widget.Button; import android.widget.EditText; import android.widget.Spinner; import android.widget.TextView; +import android.widget.Toast; import java.util.ArrayList; @@ -36,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; @@ -54,6 +60,7 @@ public class SingleUploadFragment extends DaggerFragment { @BindView(R.id.licenseSpinner) Spinner licenseSpinner; @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; private String license; private OnUploadActionInitiated uploadActionInitiatedHandler; @@ -62,9 +69,6 @@ public class SingleUploadFragment extends DaggerFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.activity_share, menu); - if (titleEdit != null) { - menu.findItem(R.id.menu_upload_single).setEnabled(titleEdit.getText().length() != 0); - } } @Override @@ -73,6 +77,11 @@ public class SingleUploadFragment extends DaggerFragment { //What happens when the 'submit' icon is tapped case R.id.menu_upload_single: + if (titleEdit.getText().toString().isEmpty()) { + Toast.makeText(getContext(), R.string.add_title_toast, Toast.LENGTH_LONG).show(); + return false; + } + String title = titleEdit.getText().toString(); String desc = descEdit.getText().toString(); @@ -84,7 +93,6 @@ public class SingleUploadFragment extends DaggerFragment { uploadActionInitiatedHandler.uploadActionInitiated(title, desc); return true; - } return super.onOptionsItemSelected(item); } @@ -95,6 +103,14 @@ public class SingleUploadFragment extends DaggerFragment { View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false); ButterKnife.bind(this, rootView); + Intent activityIntent = getActivity().getIntent(); + if (activityIntent.hasExtra("title")) { + titleEdit.setText(activityIntent.getStringExtra("title")); + } + if (activityIntent.hasExtra("description")) { + descEdit.setText(activityIntent.getStringExtra("description")); + } + ArrayList licenseItems = new ArrayList<>(); licenseItems.add(getString(R.string.license_name_cc0)); licenseItems.add(getString(R.string.license_name_cc_by)); @@ -104,6 +120,18 @@ public class SingleUploadFragment extends DaggerFragment { license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); + // If this is a direct upload from Nearby, autofill title and desc fields with the Place's values + boolean isNearbyUpload = ((ShareActivity) getActivity()).isNearbyUpload(); + + if (isNearbyUpload) { + String imageTitle = directPrefs.getString("Title", ""); + String imageDesc = directPrefs.getString("Desc", ""); + String imageCats = directPrefs.getString("Category", ""); + Timber.d("Image title: " + imageTitle + ", image desc: " + imageDesc + ", image categories: " + imageCats); + titleEdit.setText(imageTitle); + descEdit.setText(imageDesc); + } + // check if this is the first time we have uploaded if (prefs.getString("Title", "").trim().length() == 0 && prefs.getString("Desc", "").trim().length() == 0) { @@ -136,11 +164,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); @@ -208,39 +254,45 @@ public class SingleUploadFragment extends DaggerFragment { */ @OnTouch(R.id.titleEdit) boolean titleInfo(View view, MotionEvent motionEvent) { - //Should replace right with end to support different right-to-left languages as well - final int value = titleEdit.getRight() - titleEdit.getCompoundDrawables()[2].getBounds().width(); - - if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) { - new AlertDialog.Builder(getContext()) - .setTitle(R.string.media_detail_title) - .setMessage(R.string.title_info) - .setCancelable(true) - .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) - .create() - .show(); - return true; + final int value; + if (ViewCompat.getLayoutDirection(getView()) == ViewCompat.LAYOUT_DIRECTION_LTR) { + value = titleEdit.getRight() - titleEdit.getCompoundDrawables()[2].getBounds().width(); + if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) { + showInfoAlert(R.string.media_detail_title, R.string.title_info); + return true; + } + } + else { + value = titleEdit.getLeft() + titleEdit.getCompoundDrawables()[0].getBounds().width(); + if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) { + showInfoAlert(R.string.media_detail_title, R.string.title_info); + return true; + } } return false; } @OnTouch(R.id.descEdit) boolean descriptionInfo(View view, MotionEvent motionEvent) { - final int value = descEdit.getRight() - descEdit.getCompoundDrawables()[2].getBounds().width(); - - if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) { - new AlertDialog.Builder(getContext()) - .setTitle(R.string.media_detail_description) - .setMessage(R.string.description_info) - .setCancelable(true) - .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) - .create() - .show(); - return true; + final int value; + if (ViewCompat.getLayoutDirection(getView()) == ViewCompat.LAYOUT_DIRECTION_LTR) { + value = descEdit.getRight() - descEdit.getCompoundDrawables()[2].getBounds().width(); + if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) { + showInfoAlert(R.string.media_detail_description,R.string.description_info); + return true; + } + } + else{ + value = descEdit.getLeft() + descEdit.getCompoundDrawables()[0].getBounds().width(); + if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) { + showInfoAlert(R.string.media_detail_description,R.string.description_info); + return true; + } } return false; } + @SuppressLint("StringFormatInvalid") private void setLicenseSummary(String license) { licenseSummaryView.setText(getString(R.string.share_license_summary, getString(Utils.licenseNameFor(license)))); } @@ -301,4 +353,14 @@ public class SingleUploadFragment extends DaggerFragment { } } } + + private void showInfoAlert (int titleStringID, int messageStringID){ + new AlertDialog.Builder(getContext()) + .setTitle(titleStringID) + .setMessage(messageStringID) + .setCancelable(true) + .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) + .create() + .show(); + } } 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 ade22ece6..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; @@ -49,7 +50,7 @@ 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; } @@ -81,13 +82,14 @@ public class UploadController { /** * 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 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 + * @param onComplete the progress tracker */ public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) { Contribution contribution; @@ -106,8 +108,9 @@ public class UploadController { /** * Starts a new upload task. + * * @param contribution the contribution object - * @param onComplete the progress tracker + * @param onComplete the progress tracker */ public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) { //Set creator, desc, and license @@ -134,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: "); @@ -152,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("*")) { @@ -199,6 +204,7 @@ 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 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 023252b84..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; @@ -52,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; @@ -67,7 +66,6 @@ public class UploadService extends HandlerService { public static final int NOTIFICATION_UPLOAD_IN_PROGRESS = 1; public static final int NOTIFICATION_UPLOAD_COMPLETE = 2; public static final int NOTIFICATION_UPLOAD_FAILED = 3; - private ContributionDao dao; public UploadService() { super("UploadService"); @@ -107,7 +105,7 @@ public class UploadService extends HandlerService { startForeground(NOTIFICATION_UPLOAD_IN_PROGRESS, curProgressNotification.build()); contribution.setTransferred(transferred); - dao.save(contribution); + contributionDao.save(contribution); } } @@ -115,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); } @@ -124,8 +121,6 @@ public class UploadService extends HandlerService { super.onCreate(); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - contributionsProviderClient = this.getContentResolver().acquireContentProviderClient(ContributionsContentProvider.AUTHORITY); - dao = new ContributionDao(contributionsProviderClient); } @Override @@ -147,7 +142,7 @@ public class UploadService extends HandlerService { contribution.setState(Contribution.STATE_QUEUED); contribution.setTransferred(0); - dao.save(contribution); + contributionDao.save(contribution); toUpload++; if (curProgressNotification != null && toUpload != 1) { curProgressNotification.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload)); @@ -166,7 +161,7 @@ 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(ContributionDao.Table.COLUMN_STATE, Contribution.STATE_FAILED); @@ -262,7 +257,7 @@ public class UploadService extends HandlerService { contribution.setImageUrl(uploadResult.getImageUrl()); contribution.setState(Contribution.STATE_COMPLETED); contribution.setDateUploaded(uploadResult.getDateUploaded()); - dao.save(contribution); + contributionDao.save(contribution); } } catch (IOException e) { Timber.d("I have a network fuckup"); @@ -274,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); } } @@ -293,7 +288,7 @@ public class UploadService extends HandlerService { notificationManager.notify(NOTIFICATION_UPLOAD_FAILED, failureNotification); contribution.setState(Contribution.STATE_FAILED); - dao.save(contribution); + 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/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..4f6a6d456 --- /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.v("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..82227c59a 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 @@ -1,17 +1,12 @@ package fr.free.nrw.commons.utils; -import android.content.Context; -import android.support.annotation.StringRes; -import android.widget.Toast; +import android.support.design.widget.Snackbar; +import android.view.View; 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(); - } - }); + public static void showSnackbar(View view, int messageResourceId) { + Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show(); } + } diff --git a/app/src/main/res/anim/fab_close.xml b/app/src/main/res/anim/fab_close.xml new file mode 100644 index 000000000..76dd14dcb --- /dev/null +++ b/app/src/main/res/anim/fab_close.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fab_open.xml b/app/src/main/res/anim/fab_open.xml new file mode 100644 index 000000000..e23180643 --- /dev/null +++ b/app/src/main/res/anim/fab_open.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/rotate_backward.xml b/app/src/main/res/anim/rotate_backward.xml new file mode 100644 index 000000000..9bbb510ba --- /dev/null +++ b/app/src/main/res/anim/rotate_backward.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/rotate_forward.xml b/app/src/main/res/anim/rotate_forward.xml new file mode 100644 index 000000000..c1173e870 --- /dev/null +++ b/app/src/main/res/anim/rotate_forward.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_down.xml b/app/src/main/res/anim/slide_down.xml new file mode 100644 index 000000000..049b3e053 --- /dev/null +++ b/app/src/main/res/anim/slide_down.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_up.xml b/app/src/main/res/anim/slide_up.xml new file mode 100644 index 000000000..336f9ec9d --- /dev/null +++ b/app/src/main/res/anim/slide_up.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/button_color_selector.xml b/app/src/main/res/color/button_color_selector.xml new file mode 100644 index 000000000..949930952 --- /dev/null +++ b/app/src/main/res/color/button_color_selector.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/color/text_color_selector.xml b/app/src/main/res/color/text_color_selector.xml new file mode 100644 index 000000000..4a97c6773 --- /dev/null +++ b/app/src/main/res/color/text_color_selector.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-mdpi/button_background_selector.xml b/app/src/main/res/drawable-mdpi/button_background_selector.xml new file mode 100644 index 000000000..bfcb3852b --- /dev/null +++ b/app/src/main/res/drawable-mdpi/button_background_selector.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/ic_directions_black_48dp.xml b/app/src/main/res/drawable-xhdpi/ic_directions_black_48dp.xml new file mode 100644 index 000000000..b0dbfcbc7 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_directions_black_48dp.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable-xhdpi/ic_wikidata_logo_48dp.xml b/app/src/main/res/drawable-xhdpi/ic_wikidata_logo_48dp.xml new file mode 100644 index 000000000..c0074ee91 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_wikidata_logo_48dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable-xhdpi/ic_wikipedia_logo_48dp.xml b/app/src/main/res/drawable-xhdpi/ic_wikipedia_logo_48dp.xml new file mode 100644 index 000000000..fcd9edb9a --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_wikipedia_logo_48dp.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/current_location_marker.png b/app/src/main/res/drawable/current_location_marker.png new file mode 100644 index 000000000..7dcd14c25 Binary files /dev/null and b/app/src/main/res/drawable/current_location_marker.png differ diff --git a/app/src/main/res/drawable/ic_action_facebook.xml b/app/src/main/res/drawable/ic_action_facebook.xml new file mode 100644 index 000000000..e17144640 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_facebook.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_action_github.xml b/app/src/main/res/drawable/ic_action_github.xml new file mode 100644 index 000000000..569994ebf --- /dev/null +++ b/app/src/main/res/drawable/ic_action_github.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_action_website.xml b/app/src/main/res/drawable/ic_action_website.xml new file mode 100644 index 000000000..bad3beb94 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_website.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_add_white_24dp.xml b/app/src/main/res/drawable/ic_add_white_24dp.xml new file mode 100644 index 000000000..b9b8eca8b --- /dev/null +++ b/app/src/main/res/drawable/ic_add_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_commons_icon_vector.xml b/app/src/main/res/drawable/ic_commons_icon_vector.xml new file mode 100644 index 000000000..5af95beec --- /dev/null +++ b/app/src/main/res/drawable/ic_commons_icon_vector.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_directions_black_24dp.xml b/app/src/main/res/drawable/ic_directions_black_24dp.xml new file mode 100644 index 000000000..fe95ed7b1 --- /dev/null +++ b/app/src/main/res/drawable/ic_directions_black_24dp.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_help_black_24dp.xml b/app/src/main/res/drawable/ic_help_black_24dp.xml new file mode 100644 index 000000000..1517747d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_help_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_my_location_black_24dp.xml b/app/src/main/res/drawable/ic_my_location_black_24dp.xml new file mode 100644 index 000000000..07d6e4694 --- /dev/null +++ b/app/src/main/res/drawable/ic_my_location_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_overflow.xml b/app/src/main/res/drawable/ic_overflow.xml new file mode 100644 index 000000000..c3f27d467 --- /dev/null +++ b/app/src/main/res/drawable/ic_overflow.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_wikidata_logo_24dp.xml b/app/src/main/res/drawable/ic_wikidata_logo_24dp.xml new file mode 100644 index 000000000..f92da86c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_wikidata_logo_24dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_wikipedia_logo_24dp.xml b/app/src/main/res/drawable/ic_wikipedia_logo_24dp.xml new file mode 100644 index 000000000..9056d87fa --- /dev/null +++ b/app/src/main/res/drawable/ic_wikipedia_logo_24dp.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout-land/activity_login.xml b/app/src/main/res/layout-land/activity_login.xml index 9ecaf9855..deb295438 100644 --- a/app/src/main/res/layout-land/activity_login.xml +++ b/app/src/main/res/layout-land/activity_login.xml @@ -13,12 +13,12 @@ @@ -40,11 +40,26 @@ android:textColor="@android:color/white" android:textSize="@dimen/heading_text_size" /> + + @@ -56,6 +71,7 @@ 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" @@ -98,7 +114,7 @@ android:layout_marginLeft="@dimen/standard_gap" android:layout_marginRight="@dimen/standard_gap" android:layout_marginStart="@dimen/standard_gap" - app:passwordToggleEnabled="false"> + app:passwordToggleEnabled="true"> + + + android:layout_marginBottom="@dimen/standard_gap" + android:text="@string/about_privacy_policy" /> @@ -188,19 +215,19 @@ + android:elevation="8dp" + app:srcCompat="@drawable/blue_rinse_circle" + tools:ignore="UnusedAttribute" /> + app:srcCompat="@drawable/commons_logo_large" + tools:ignore="UnusedAttribute" /> diff --git a/app/src/main/res/layout-land/activity_nearby.xml b/app/src/main/res/layout-land/activity_nearby.xml new file mode 100644 index 000000000..c945193ba --- /dev/null +++ b/app/src/main/res/layout-land/activity_nearby.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-land/welcome_do_upload.xml b/app/src/main/res/layout-land/welcome_do_upload.xml index 1f02c3183..01e5c2af3 100644 --- a/app/src/main/res/layout-land/welcome_do_upload.xml +++ b/app/src/main/res/layout-land/welcome_do_upload.xml @@ -1,88 +1,189 @@ - + + + + + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toStartOf="@id/center_guideline"> + android:scaleType="fitXY" + android:src="@drawable/mount_zao" /> + android:scaleType="fitXY" + android:src="@drawable/llamas" /> + android:scaleType="fitXY" + android:src="@drawable/rainbow_bridge" /> + android:scaleType="fitXY" + android:src="@drawable/tulip" /> - - + + + + + + + android:textSize="16sp" /> + + + + + + + + + + + + + + + + + + + + + - + + diff --git a/app/src/main/res/layout-land/welcome_dont_upload.xml b/app/src/main/res/layout-land/welcome_dont_upload.xml index 1d4d0db80..6d954e2d7 100644 --- a/app/src/main/res/layout-land/welcome_dont_upload.xml +++ b/app/src/main/res/layout-land/welcome_dont_upload.xml @@ -1,70 +1,164 @@ - + xmlns:app="http://schemas.android.com/apk/res-auto" + android:background="#0c609c"> - + android:layout_marginTop="@dimen/standard_gap" + android:text="@string/welcome_skip_button" + android:textColor="#fff" + android:textSize="@dimen/normal_text" + android:textStyle="bold" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + + + android:src="@drawable/selfie_x" /> + android:src="@drawable/proprietary_x" /> - + - - + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@id/center_guideline" + app:layout_constraintTop_toTopOf="parent" + android:orientation="vertical"> + android:textSize="@dimen/normal_text" + android:textStyle="bold" /> + + + + + + + + + + + + + + + + + + + + + + + + + - + + diff --git a/app/src/main/res/layout-land/welcome_image_details.xml b/app/src/main/res/layout-land/welcome_image_details.xml index d112292a1..e7d704e15 100644 --- a/app/src/main/res/layout-land/welcome_image_details.xml +++ b/app/src/main/res/layout-land/welcome_image_details.xml @@ -1,53 +1,153 @@ - + android:orientation="horizontal"> - + android:layout_height="@dimen/overflow_button_dimen" + android:layout_marginEnd="@dimen/standard_gap" + android:layout_marginRight="@dimen/standard_gap" + android:layout_marginTop="@dimen/standard_gap" + android:text="@string/welcome_skip_button" + android:textColor="#fff" + android:textSize="@dimen/normal_text" + android:textStyle="bold" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - - + android:orientation="vertical" + app:layout_constraintGuide_percent="0.60" /> + + + + + android:textSize="@dimen/normal_text" + android:textStyle="bold" /> + + + + + + + + + + + + + + + + + + + + + + + + + - + + diff --git a/app/src/main/res/layout-land/welcome_wikipedia.xml b/app/src/main/res/layout-land/welcome_wikipedia.xml index b4fbe9048..73b96fc14 100644 --- a/app/src/main/res/layout-land/welcome_wikipedia.xml +++ b/app/src/main/res/layout-land/welcome_wikipedia.xml @@ -1,51 +1,76 @@ - + + + + + + + + + + + + - - - - - + android:maxWidth="240dp" + android:paddingTop="@dimen/standard_gap" + android:text="@string/tutorial_1_subtext" + android:textAlignment="center" + android:textColor="@android:color/white" /> - + diff --git a/app/src/main/res/layout-xlarge/activity_login.xml b/app/src/main/res/layout-xlarge/activity_login.xml index 32cd47451..819260920 100644 --- a/app/src/main/res/layout-xlarge/activity_login.xml +++ b/app/src/main/res/layout-xlarge/activity_login.xml @@ -13,12 +13,12 @@ @@ -40,6 +40,21 @@ android:textColor="@android:color/white" android:textSize="@dimen/heading_text_size" /> + + + + + android:layout_marginBottom="@dimen/standard_gap" + android:text="@string/about_privacy_policy" /> @@ -188,8 +215,8 @@ @@ -198,9 +225,9 @@ android:layout_height="42dp" android:layout_gravity="center_horizontal" android:layout_marginTop="8dp" - tools:ignore="UnusedAttribute" android:elevation="8dp" - app:srcCompat="@drawable/commons_logo_large" /> + app:srcCompat="@drawable/commons_logo_large" + tools:ignore="UnusedAttribute" /> diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index bbbced3e9..9551e6e64 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -5,81 +5,154 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:layout_height="wrap_content"> - + android:layout_height="wrap_content"> - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - + + - + + + + - + + + app:menu="@menu/drawer" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_contributions.xml b/app/src/main/res/layout/activity_contributions.xml index 9065618f2..51a48f0a5 100644 --- a/app/src/main/res/layout/activity_contributions.xml +++ b/app/src/main/res/layout/activity_contributions.xml @@ -15,9 +15,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> - - + @@ -40,11 +49,27 @@ android:textColor="@android:color/white" android:textSize="@dimen/heading_text_size" /> + + + @@ -56,6 +81,7 @@ 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" @@ -98,7 +124,7 @@ android:layout_marginLeft="@dimen/standard_gap" android:layout_marginRight="@dimen/standard_gap" android:layout_marginStart="@dimen/standard_gap" - app:passwordToggleEnabled="false"> + app:passwordToggleEnabled="true"> + android:text="@string/forgot_password" /> + + android:elevation="8dp" + app:srcCompat="@drawable/blue_rinse_circle" + tools:ignore="UnusedAttribute" /> - + app:srcCompat="@drawable/commons_logo_large" + tools:ignore="UnusedAttribute" /> + + + + diff --git a/app/src/main/res/layout/activity_multiple_uploads.xml b/app/src/main/res/layout/activity_multiple_uploads.xml index 115c2cf89..2e35444c0 100644 --- a/app/src/main/res/layout/activity_multiple_uploads.xml +++ b/app/src/main/res/layout/activity_multiple_uploads.xml @@ -15,8 +15,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> - - - + android:layout_height="match_parent"> - - - - + android:layout_height="match_parent" + android:gravity="center_vertical" + android:orientation="horizontal"> + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + app:menu="@menu/drawer" /> \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_details.xml b/app/src/main/res/layout/bottom_sheet_details.xml new file mode 100644 index 000000000..2be0f6cf7 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_details.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/bottom_sheet_nearby.xml b/app/src/main/res/layout/bottom_sheet_nearby.xml new file mode 100644 index 000000000..11d6762d3 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_nearby.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/app/src/main/res/layout/detail_category_item.xml b/app/src/main/res/layout/detail_category_item.xml index 51302fd0d..ad6a7fca4 100644 --- a/app/src/main/res/layout/detail_category_item.xml +++ b/app/src/main/res/layout/detail_category_item.xml @@ -21,9 +21,4 @@ app:drawableStart="@drawable/ic_info_outline_white_24dp" /> - - diff --git a/app/src/main/res/layout/fragment_categorization.xml b/app/src/main/res/layout/fragment_categorization.xml index 880d0d5be..58a768094 100644 --- a/app/src/main/res/layout/fragment_categorization.xml +++ b/app/src/main/res/layout/fragment_categorization.xml @@ -1,32 +1,34 @@ + android:theme="@style/DarkAppTheme"> + android:layout_height="wrap_content" + android:clickable="true" + android:focusableInTouchMode="true"> + android:gravity="left" + android:inputType="textCapWords" + android:imeOptions="flagNoExtractUi"/> @@ -44,27 +46,24 @@ + android:visibility="gone" /> + android:visibility="gone" /> + android:layout_height="wrap_content" + android:fadingEdge="none" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_media_detail.xml b/app/src/main/res/layout/fragment_media_detail.xml index 392ea6bef..6b530af1c 100644 --- a/app/src/main/res/layout/fragment_media_detail.xml +++ b/app/src/main/res/layout/fragment_media_detail.xml @@ -182,6 +182,10 @@ tools:text="License link" /> + + + + +